* Reorganised the includes directory, creating subdirectories db, parser and specials
authorTim Starling <tstarling@users.mediawiki.org>
Mon, 16 Jun 2008 20:21:26 +0000 (20:21 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Mon, 16 Jun 2008 20:21:26 +0000 (20:21 +0000)
* Wrote a tool to check the integrity of the autoloader class list, fixed some issues that came up.
* Start the autoloader before LocalSettings.php, so that when an extension writer thinks an inefficient one-file special page extension is the way to go, they don't have to use explicit includes to make the class inheritance work. Should continue to work with $IP set in LocalSettings.php as long as $IP is set before extensions are included.

197 files changed:
includes/AutoLoader.php
includes/CoreParserFunctions.php [deleted file]
includes/Database.php [deleted file]
includes/DatabaseMssql.php [deleted file]
includes/DatabaseOracle.php [deleted file]
includes/DatabasePostgres.php [deleted file]
includes/DatabaseSqlite.php [deleted file]
includes/DateFormatter.php [deleted file]
includes/LBFactory.php [deleted file]
includes/LBFactory_Multi.php [deleted file]
includes/LoadBalancer.php [deleted file]
includes/Parser.php [deleted file]
includes/ParserCache.php [deleted file]
includes/ParserOptions.php [deleted file]
includes/ParserOutput.php [deleted file]
includes/Parser_DiffTest.php [deleted file]
includes/Parser_OldPP.php [deleted file]
includes/Preprocessor.php [deleted file]
includes/Preprocessor_DOM.php [deleted file]
includes/Preprocessor_Hash.php [deleted file]
includes/Setup.php
includes/SpecialAllmessages.php [deleted file]
includes/SpecialAllpages.php [deleted file]
includes/SpecialAncientpages.php [deleted file]
includes/SpecialBlockip.php [deleted file]
includes/SpecialBlockme.php [deleted file]
includes/SpecialBooksources.php [deleted file]
includes/SpecialBrokenRedirects.php [deleted file]
includes/SpecialCategories.php [deleted file]
includes/SpecialConfirmemail.php [deleted file]
includes/SpecialContributions.php [deleted file]
includes/SpecialDeadendpages.php [deleted file]
includes/SpecialDisambiguations.php [deleted file]
includes/SpecialDoubleRedirects.php [deleted file]
includes/SpecialEmailuser.php [deleted file]
includes/SpecialExport.php [deleted file]
includes/SpecialFewestrevisions.php [deleted file]
includes/SpecialFileDuplicateSearch.php [deleted file]
includes/SpecialFilepath.php [deleted file]
includes/SpecialImagelist.php [deleted file]
includes/SpecialImport.php [deleted file]
includes/SpecialIpblocklist.php [deleted file]
includes/SpecialListgrouprights.php [deleted file]
includes/SpecialListredirects.php [deleted file]
includes/SpecialListusers.php [deleted file]
includes/SpecialLockdb.php [deleted file]
includes/SpecialLog.php [deleted file]
includes/SpecialLonelypages.php [deleted file]
includes/SpecialLongpages.php [deleted file]
includes/SpecialMIMEsearch.php [deleted file]
includes/SpecialMergeHistory.php [deleted file]
includes/SpecialMissingFiles.php [deleted file]
includes/SpecialMostcategories.php [deleted file]
includes/SpecialMostimages.php [deleted file]
includes/SpecialMostlinked.php [deleted file]
includes/SpecialMostlinkedcategories.php [deleted file]
includes/SpecialMostlinkedtemplates.php [deleted file]
includes/SpecialMostrevisions.php [deleted file]
includes/SpecialMovepage.php [deleted file]
includes/SpecialNewimages.php [deleted file]
includes/SpecialNewpages.php [deleted file]
includes/SpecialPage.php
includes/SpecialPopularpages.php [deleted file]
includes/SpecialPreferences.php [deleted file]
includes/SpecialPrefixindex.php [deleted file]
includes/SpecialProtectedpages.php [deleted file]
includes/SpecialProtectedtitles.php [deleted file]
includes/SpecialRandompage.php [deleted file]
includes/SpecialRandomredirect.php [deleted file]
includes/SpecialRecentchanges.php [deleted file]
includes/SpecialRecentchangeslinked.php [deleted file]
includes/SpecialResetpass.php [deleted file]
includes/SpecialRevisiondelete.php [deleted file]
includes/SpecialSearch.php [deleted file]
includes/SpecialShortpages.php [deleted file]
includes/SpecialSpecialpages.php [deleted file]
includes/SpecialStatistics.php [deleted file]
includes/SpecialUncategorizedcategories.php [deleted file]
includes/SpecialUncategorizedimages.php [deleted file]
includes/SpecialUncategorizedpages.php [deleted file]
includes/SpecialUncategorizedtemplates.php [deleted file]
includes/SpecialUndelete.php [deleted file]
includes/SpecialUnlockdb.php [deleted file]
includes/SpecialUnusedcategories.php [deleted file]
includes/SpecialUnusedimages.php [deleted file]
includes/SpecialUnusedtemplates.php [deleted file]
includes/SpecialUnwatchedpages.php [deleted file]
includes/SpecialUpload.php [deleted file]
includes/SpecialUploadMogile.php [deleted file]
includes/SpecialUserlogin.php [deleted file]
includes/SpecialUserlogout.php [deleted file]
includes/SpecialUserrights.php [deleted file]
includes/SpecialVersion.php [deleted file]
includes/SpecialWantedcategories.php [deleted file]
includes/SpecialWantedpages.php [deleted file]
includes/SpecialWatchlist.php [deleted file]
includes/SpecialWhatlinkshere.php [deleted file]
includes/SpecialWithoutinterwiki.php [deleted file]
includes/WebStart.php
includes/db/Database.php [new file with mode: 0644]
includes/db/DatabaseMssql.php [new file with mode: 0755]
includes/db/DatabaseOracle.php [new file with mode: 0644]
includes/db/DatabasePostgres.php [new file with mode: 0644]
includes/db/DatabaseSqlite.php [new file with mode: 0644]
includes/db/LBFactory.php [new file with mode: 0644]
includes/db/LBFactory_Multi.php [new file with mode: 0644]
includes/db/LoadBalancer.php [new file with mode: 0644]
includes/parser/CoreParserFunctions.php [new file with mode: 0644]
includes/parser/DateFormatter.php [new file with mode: 0644]
includes/parser/Parser.php [new file with mode: 0644]
includes/parser/ParserCache.php [new file with mode: 0644]
includes/parser/ParserOptions.php [new file with mode: 0644]
includes/parser/ParserOutput.php [new file with mode: 0644]
includes/parser/Parser_DiffTest.php [new file with mode: 0644]
includes/parser/Parser_OldPP.php [new file with mode: 0644]
includes/parser/Preprocessor.php [new file with mode: 0644]
includes/parser/Preprocessor_DOM.php [new file with mode: 0644]
includes/parser/Preprocessor_Hash.php [new file with mode: 0644]
includes/specials/Allmessages.php [new file with mode: 0644]
includes/specials/Allpages.php [new file with mode: 0644]
includes/specials/Ancientpages.php [new file with mode: 0644]
includes/specials/Blockip.php [new file with mode: 0644]
includes/specials/Blockme.php [new file with mode: 0644]
includes/specials/Booksources.php [new file with mode: 0644]
includes/specials/BrokenRedirects.php [new file with mode: 0644]
includes/specials/Categories.php [new file with mode: 0644]
includes/specials/Confirmemail.php [new file with mode: 0644]
includes/specials/Contributions.php [new file with mode: 0644]
includes/specials/Deadendpages.php [new file with mode: 0644]
includes/specials/Disambiguations.php [new file with mode: 0644]
includes/specials/DoubleRedirects.php [new file with mode: 0644]
includes/specials/Emailuser.php [new file with mode: 0644]
includes/specials/Export.php [new file with mode: 0644]
includes/specials/Fewestrevisions.php [new file with mode: 0644]
includes/specials/FileDuplicateSearch.php [new file with mode: 0644]
includes/specials/Filepath.php [new file with mode: 0644]
includes/specials/Imagelist.php [new file with mode: 0644]
includes/specials/Import.php [new file with mode: 0644]
includes/specials/Ipblocklist.php [new file with mode: 0644]
includes/specials/Listgrouprights.php [new file with mode: 0644]
includes/specials/Listredirects.php [new file with mode: 0644]
includes/specials/Listusers.php [new file with mode: 0644]
includes/specials/Lockdb.php [new file with mode: 0644]
includes/specials/Log.php [new file with mode: 0644]
includes/specials/Lonelypages.php [new file with mode: 0644]
includes/specials/Longpages.php [new file with mode: 0644]
includes/specials/MIMEsearch.php [new file with mode: 0644]
includes/specials/MergeHistory.php [new file with mode: 0644]
includes/specials/MissingFiles.php [new file with mode: 0644]
includes/specials/Mostcategories.php [new file with mode: 0644]
includes/specials/Mostimages.php [new file with mode: 0644]
includes/specials/Mostlinked.php [new file with mode: 0644]
includes/specials/Mostlinkedcategories.php [new file with mode: 0644]
includes/specials/Mostlinkedtemplates.php [new file with mode: 0644]
includes/specials/Mostrevisions.php [new file with mode: 0644]
includes/specials/Movepage.php [new file with mode: 0644]
includes/specials/Newimages.php [new file with mode: 0644]
includes/specials/Newpages.php [new file with mode: 0644]
includes/specials/Popularpages.php [new file with mode: 0644]
includes/specials/Preferences.php [new file with mode: 0644]
includes/specials/Prefixindex.php [new file with mode: 0644]
includes/specials/Protectedpages.php [new file with mode: 0644]
includes/specials/Protectedtitles.php [new file with mode: 0644]
includes/specials/Randompage.php [new file with mode: 0644]
includes/specials/Randomredirect.php [new file with mode: 0644]
includes/specials/Recentchanges.php [new file with mode: 0644]
includes/specials/Recentchangeslinked.php [new file with mode: 0644]
includes/specials/Resetpass.php [new file with mode: 0644]
includes/specials/Revisiondelete.php [new file with mode: 0644]
includes/specials/Search.php [new file with mode: 0644]
includes/specials/Shortpages.php [new file with mode: 0644]
includes/specials/Specialpages.php [new file with mode: 0644]
includes/specials/Statistics.php [new file with mode: 0644]
includes/specials/Uncategorizedcategories.php [new file with mode: 0644]
includes/specials/Uncategorizedimages.php [new file with mode: 0644]
includes/specials/Uncategorizedpages.php [new file with mode: 0644]
includes/specials/Uncategorizedtemplates.php [new file with mode: 0644]
includes/specials/Undelete.php [new file with mode: 0644]
includes/specials/Unlockdb.php [new file with mode: 0644]
includes/specials/Unusedcategories.php [new file with mode: 0644]
includes/specials/Unusedimages.php [new file with mode: 0644]
includes/specials/Unusedtemplates.php [new file with mode: 0644]
includes/specials/Unwatchedpages.php [new file with mode: 0644]
includes/specials/Upload.php [new file with mode: 0644]
includes/specials/UploadMogile.php [new file with mode: 0644]
includes/specials/Userlogin.php [new file with mode: 0644]
includes/specials/Userlogout.php [new file with mode: 0644]
includes/specials/Userrights.php [new file with mode: 0644]
includes/specials/Version.php [new file with mode: 0644]
includes/specials/Wantedcategories.php [new file with mode: 0644]
includes/specials/Wantedpages.php [new file with mode: 0644]
includes/specials/Watchlist.php [new file with mode: 0644]
includes/specials/Whatlinkshere.php [new file with mode: 0644]
includes/specials/Withoutinterwiki.php [new file with mode: 0644]
includes/templates/Userlogin.php
maintenance/checkAutoLoader.php [new file with mode: 0644]
maintenance/commandLine.inc

index 1e0fd37..a823057 100644 (file)
@@ -4,18 +4,14 @@
 
 ini_set('unserialize_callback_func', '__autoload' );
 
-function __autoload($className) {
-       global $wgAutoloadClasses;
-
+class AutoLoader {
        # Locations of core classes
        # Extension classes are specified with $wgAutoloadClasses
        static $localClasses = array(
                # Includes
-               'AjaxCachePolicy' => 'includes/AjaxFunctions.php',
                'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
                'AjaxResponse' => 'includes/AjaxResponse.php',
                'AlphabeticPager' => 'includes/Pager.php',
-               'AncientPagesPage' => 'includes/SpecialAncientpages.php',
                'APCBagOStuff' => 'includes/BagOStuff.php',
                'ArrayDiffFormatter' => 'includes/DifferenceEngine.php',
                'Article' => 'includes/Article.php',
@@ -24,29 +20,16 @@ function __autoload($className) {
                'Autopromote' => 'includes/Autopromote.php',
                'BagOStuff' => 'includes/BagOStuff.php',
                'Block' => 'includes/Block.php',
-               'BrokenRedirectsPage' => 'includes/SpecialBrokenRedirects.php',
+               'CacheDependency' => 'includes/CacheDependency.php',
                'Category' => 'includes/Category.php',
                'Categoryfinder' => 'includes/Categoryfinder.php',
                'CategoryPage' => 'includes/CategoryPage.php',
                'CategoryViewer' => 'includes/CategoryPage.php',
                'ChangesList' => 'includes/ChangesList.php',
                'ChannelFeed' => 'includes/Feed.php',
-               'ChronologyProtector' => 'includes/LBFactory.php',
                'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
-               'ContributionsPage' => 'includes/SpecialContributions.php',
-               'CoreParserFunctions' => 'includes/CoreParserFunctions.php',
-               'Database' => 'includes/Database.php',
-               'DatabaseMysql' => 'includes/Database.php',
-               'DatabaseOracle' => 'includes/DatabaseOracle.php',
-               'DatabasePostgres' => 'includes/DatabasePostgres.php',
-               'DatabaseSqlite' => 'includes/DatabaseSqlite.php',
-               'DatabaseMssql' => 'includes/DatabaseMssql.php',
-               'DateFormatter' => 'includes/DateFormatter.php',
+               'ConstantDependency' => 'includes/CacheDependency.php',
                'DBABagOStuff' => 'includes/BagOStuff.php',
-               'DBLockForm' => 'includes/SpecialLockdb.php',
-               'DBObject' => 'includes/Database.php',
-               'DBUnlockForm' => 'includes/SpecialUnlockdb.php',
-               'DeadendPagesPage' => 'includes/SpecialDeadendpages.php',
                'DependencyWrapper' => 'includes/CacheDependency.php',
                '_DiffEngine' => 'includes/DifferenceEngine.php',
                'DifferenceEngine' => 'includes/DifferenceEngine.php',
@@ -57,9 +40,7 @@ function __autoload($className) {
                '_DiffOp_Copy' => 'includes/DifferenceEngine.php',
                '_DiffOp_Delete' => 'includes/DifferenceEngine.php',
                '_DiffOp' => 'includes/DifferenceEngine.php',
-               'DisambiguationsPage' => 'includes/SpecialDisambiguations.php',
                'DjVuImage' => 'includes/DjVuImage.php',
-               'DoubleRedirectsPage' => 'includes/SpecialDoubleRedirects.php',
                'DoubleReplacer' => 'includes/StringUtils.php',
                'Dump7ZipOutput' => 'includes/Export.php',
                'DumpBZip2Output' => 'includes/Export.php',
@@ -74,33 +55,29 @@ function __autoload($className) {
                'DumpPipeOutput' => 'includes/Export.php',
                'eAccelBagOStuff' => 'includes/BagOStuff.php',
                'EditPage' => 'includes/EditPage.php',
-               'EmailConfirmation' => 'includes/SpecialConfirmemail.php',
-               'EmailInvalidation' => 'includes/SpecialConfirmemail.php',
                'EmaillingJob' => 'includes/EmaillingJob.php',
-               'EmaillingJob' => 'includes/JobQueue.php',
                'EmailNotification' => 'includes/UserMailer.php',
-               'EmailUserForm' => 'includes/SpecialEmailuser.php',
                'EnhancedChangesList' => 'includes/ChangesList.php',
                'EnotifNotifyJob' => 'includes/EnotifNotifyJob.php',
+               'ErrorPageError' => 'includes/Exception.php',
                'Exif' => 'includes/Exif.php',
                'ExternalEdit' => 'includes/ExternalEdit.php',
                'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
                'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
                'ExternalStore' => 'includes/ExternalStore.php',
-               'FakeMemCachedClient' => 'includes/ObjectCache.php',
+               'FatalError' => 'includes/Exception.php',
                'FakeTitle' => 'includes/FakeTitle.php',
                'FauxRequest' => 'includes/WebRequest.php',
                'FeedItem' => 'includes/Feed.php',
-               'FewestrevisionsPage' => 'includes/SpecialFewestrevisions.php',
                'FileDeleteForm' => 'includes/FileDeleteForm.php',
                'FileDependency' => 'includes/CacheDependency.php',
-               'FileDuplicateSearch' => 'includes/SpecialFileDuplicateSearch.php',
                'FileRevertForm' => 'includes/FileRevertForm.php',
                'FileStore' => 'includes/FileStore.php',
                'FormatExif' => 'includes/Exif.php',
                'FormOptions' => 'includes/FormOptions.php',
                'FSException' => 'includes/FileStore.php',
                'FSTransaction' => 'includes/FileStore.php',
+               'GlobalDependency' => 'includes/CacheDependency.php',
                'HashBagOStuff' => 'includes/BagOStuff.php',
                'HashtableReplacer' => 'includes/StringUtils.php',
                'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
@@ -115,17 +92,10 @@ function __autoload($className) {
                'ImageHistoryList' => 'includes/ImagePage.php',
                'ImagePage' => 'includes/ImagePage.php',
                'ImageQueryPage' => 'includes/ImageQueryPage.php',
-               'ImportStreamSource' => 'includes/SpecialImport.php',
-               'ImportStringSource' => 'includes/SpecialImport.php',
                'IncludableSpecialPage' => 'includes/SpecialPage.php',
                'IndexPager' => 'includes/Pager.php',
-               'IPBlockForm' => 'includes/SpecialBlockip.php',
                'IP' => 'includes/IP.php',
-               'IPUnblockForm' => 'includes/SpecialIpblocklist.php',
                'Job' => 'includes/JobQueue.php',
-               'LBFactory' => 'includes/LBFactory.php',
-               'LBFactory_Multi' => 'includes/LBFactory_Multi.php',
-               'LBFactory_Simple' => 'includes/LBFactory.php',
                'License' => 'includes/Licenses.php',
                'Licenses' => 'includes/Licenses.php',
                'LinkBatch' => 'includes/LinkBatch.php',
@@ -133,15 +103,11 @@ function __autoload($className) {
                'Linker' => 'includes/Linker.php',
                'LinkFilter' => 'includes/LinkFilter.php',
                'LinksUpdate' => 'includes/LinksUpdate.php',
-               'ListredirectsPage' => 'includes/SpecialListredirects.php',
-               'LoadBalancer' => 'includes/LoadBalancer.php',
-               'LoginForm' => 'includes/SpecialUserlogin.php',
                'LogPage' => 'includes/LogPage.php',
+               'LogPager' => 'includes/LogEventsList.php',
                'LogEventsList' => 'includes/LogEventsList.php',
                'LogReader' => 'includes/LogEventsList.php',
                'LogViewer' => 'includes/LogEventsList.php',
-               'LonelyPagesPage' => 'includes/SpecialLonelypages.php',
-               'LongPagesPage' => 'includes/SpecialLongpages.php',
                'MacBinary' => 'includes/MacBinary.php',
                'MagicWordArray' => 'includes/MagicWord.php',
                'MagicWord' => 'includes/MagicWord.php',
@@ -156,55 +122,29 @@ function __autoload($className) {
                'memcached' => 'includes/memcached-client.php',
                'MessageCache' => 'includes/MessageCache.php',
                'MimeMagic' => 'includes/MimeMagic.php',
-               'MIMEsearchPage' => 'includes/SpecialMIMEsearch.php',
-               'MostcategoriesPage' => 'includes/SpecialMostcategories.php',
-               'MostimagesPage' => 'includes/SpecialMostimages.php',
-               'MostlinkedCategoriesPage' => 'includes/SpecialMostlinkedcategories.php',
-               'MostlinkedPage' => 'includes/SpecialMostlinked.php',
-               'MostrevisionsPage' => 'includes/SpecialMostrevisions.php',
-               'MovePageForm' => 'includes/SpecialMovepage.php',
                'MWException' => 'includes/Exception.php',
                'MWNamespace' => 'includes/Namespace.php',
                'MySQLSearchResultSet' => 'includes/SearchMySQL.php',
-               'MySQLMasterPos' => 'includes/Database.php',
                'Namespace' => 'includes/NamespaceCompat.php', // Compat
-               'NewbieContributionsPage' => 'includes/SpecialNewbieContributions.php',
-               'NewPagesPage' => 'includes/SpecialNewpages.php',
                'OldChangesList' => 'includes/ChangesList.php',
+               'OracleSearchResultSet' => 'includes/SearchOracle.php',
                'OutputPage' => 'includes/OutputPage.php',
-               'PageArchive' => 'includes/SpecialUndelete.php',
                'PageHistory' => 'includes/PageHistory.php',
+               'PageHistoryPager' => 'includes/PageHistory.php',
                'PageQueryPage' => 'includes/PageQueryPage.php',
-               'ParserCache' => 'includes/ParserCache.php',
-               'Parser_DiffTest' => 'includes/Parser_DiffTest.php',
-               'Parser' => 'includes/Parser.php',
-               'Parser_OldPP' => 'includes/Parser_OldPP.php',
-               'ParserOptions' => 'includes/ParserOptions.php',
-               'ParserOutput' => 'includes/ParserOutput.php',
-               'PasswordResetForm' => 'includes/SpecialResetpass.php',
+               'Pager' => 'includes/Pager.php',
+               'PasswordError' => 'includes/User.php',
                'PatrolLog' => 'includes/PatrolLog.php',
-               'PopularPagesPage' => 'includes/SpecialPopularpages.php',
-               'PPDStackElement' => 'includes/Preprocessor_DOM.php',
-               'PPDStack' => 'includes/Preprocessor_DOM.php',
-               'PPFrame_DOM' => 'includes/Preprocessor_DOM.php',
-               'PPFrame' => 'includes/Preprocessor.php',
-               'PPNode_DOM' => 'includes/Preprocessor_DOM.php',
-               'PPNode' => 'includes/Preprocessor.php',
-               'PPTemplateFrame_DOM' => 'includes/Preprocessor_DOM.php',
-               'PreferencesForm' => 'includes/SpecialPreferences.php',
+               'PostgresSearchResult' => 'includes/SearchPostgres.php',
+               'PostgresSearchResultSet' => 'includes/SearchPostgres.php',
                'PrefixSearch' => 'includes/PrefixSearch.php',
-               'Preprocessor_DOM' => 'includes/Preprocessor_DOM.php',
-               'Preprocessor_Hash' => 'includes/Preprocessor_Hash.php',
-               'Preprocessor' => 'includes/Preprocessor.php',
                'Profiler' => 'includes/Profiler.php',
                'ProfilerSimple' => 'includes/ProfilerSimple.php',
                'ProfilerSimpleText' => 'includes/ProfilerSimpleText.php',
                'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php',
                'ProtectionForm' => 'includes/ProtectionForm.php',
-               'ProxyTools' => 'includes/ProxyTools.php',
                'QueryPage' => 'includes/QueryPage.php',
                'QuickTemplate' => 'includes/SkinTemplate.php',
-               'RandomPage' => 'includes/SpecialRandompage.php',
                'RawPage' => 'includes/RawPage.php',
                'RCCacheEntry' => 'includes/ChangesList.php',
                'RecentChange' => 'includes/RecentChange.php',
@@ -212,38 +152,32 @@ function __autoload($className) {
                'RegexlikeReplacer' => 'includes/StringUtils.php',
                'ReplacementArray' => 'includes/StringUtils.php',
                'Replacer' => 'includes/StringUtils.php',
-               'ResultWrapper' => 'includes/Database.php',
                'ReverseChronologicalPager' => 'includes/Pager.php',
-               'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php',
-               'RevisionDeleter' => 'includes/SpecialRevisiondelete.php',
                'Revision' => 'includes/Revision.php',
                'RSSFeed' => 'includes/Feed.php',
                'Sanitizer' => 'includes/Sanitizer.php',
                'SearchEngineDummy' => 'includes/SearchEngine.php',
                'SearchEngine' => 'includes/SearchEngine.php',
+               'SearchHighlighter' => 'includes/SearchEngine.php',
                'SearchMySQL4' => 'includes/SearchMySQL4.php',
                'SearchMySQL' => 'includes/SearchMySQL.php',
                'SearchOracle' => 'includes/SearchOracle.php',
                'SearchPostgres' => 'includes/SearchPostgres.php',
                'SearchResult' => 'includes/SearchEngine.php',
                'SearchResultSet' => 'includes/SearchEngine.php',
+               'SearchResultTooMany' => 'includes/SearchEngine.php',
                'SearchUpdate' => 'includes/SearchUpdate.php',
                'SearchUpdateMyISAM' => 'includes/SearchUpdate.php',
-               'ShortPagesPage' => 'includes/SpecialShortpages.php',
                'SiteConfiguration' => 'includes/SiteConfiguration.php',
                'SiteStats' => 'includes/SiteStats.php',
                'SiteStatsUpdate' => 'includes/SiteStats.php',
                'Skin' => 'includes/Skin.php',
                'SkinTemplate' => 'includes/SkinTemplate.php',
-               'SpecialAllpages' => 'includes/SpecialAllpages.php',
-               'SpecialBookSources' => 'includes/SpecialBooksources.php',
-               'SpecialListGroupRights' => 'includes/SpecialListgrouprights.php',
-               'SpecialMostlinkedtemplates' => 'includes/SpecialMostlinkedtemplates.php',
+               'SpecialMycontributions' => 'includes/SpecialPage.php',
+               'SpecialMypage' => 'includes/SpecialPage.php',
+               'SpecialMytalk' => 'includes/SpecialPage.php',
                'SpecialPage' => 'includes/SpecialPage.php',
-               'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php',
-               'SpecialRandomredirect' => 'includes/SpecialRandomredirect.php',
-               'SpecialSearch' => 'includes/SpecialSearch.php',
-               'SpecialVersion' => 'includes/SpecialVersion.php',
+               'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
                'SqlBagOStuff' => 'includes/BagOStuff.php',
                'SquidUpdate' => 'includes/SquidUpdate.php',
                'Status' => 'includes/Status.php',
@@ -256,38 +190,21 @@ function __autoload($className) {
                'TitleListDependency' => 'includes/CacheDependency.php',
                'TransformParameterError' => 'includes/MediaTransformOutput.php',
                'TurckBagOStuff' => 'includes/BagOStuff.php',
-               'UncategorizedCategoriesPage' => 'includes/SpecialUncategorizedcategories.php',
-               'UncategorizedPagesPage' => 'includes/SpecialUncategorizedpages.php',
-               'UncategorizedTemplatesPage' => 'includes/SpecialUncategorizedtemplates.php',
-               'UndeleteForm' => 'includes/SpecialUndelete.php',
                'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php',
                'UnlistedSpecialPage' => 'includes/SpecialPage.php',
-               'UnusedCategoriesPage' => 'includes/SpecialUnusedcategories.php',
-               'UnusedimagesPage' => 'includes/SpecialUnusedimages.php',
-               'UnusedtemplatesPage' => 'includes/SpecialUnusedtemplates.php',
-               'UnwatchedpagesPage' => 'includes/SpecialUnwatchedpages.php',
-               'UploadForm' => 'includes/SpecialUpload.php',
-               'UploadFormMogile' => 'includes/SpecialUploadMogile.php',
                'User' => 'includes/User.php',
                'UserArray' => 'includes/UserArray.php',
                'UserArrayFromResult' => 'includes/UserArray.php',
                'UserMailer' => 'includes/UserMailer.php',
-               'UserrightsPage' => 'includes/SpecialUserrights.php',
                'UserRightsProxy' => 'includes/UserRightsProxy.php',
-               'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php',
-               'WantedPagesPage' => 'includes/SpecialWantedpages.php',
                'WatchedItem' => 'includes/WatchedItem.php',
                'WatchlistEditor' => 'includes/WatchlistEditor.php',
                'WebRequest' => 'includes/WebRequest.php',
                'WebResponse' => 'includes/WebResponse.php',
-               'WhatLinksHerePage' => 'includes/SpecialWhatlinkshere.php',
                'WikiError' => 'includes/WikiError.php',
                'WikiErrorMsg' => 'includes/WikiError.php',
                'WikiExporter' => 'includes/Export.php',
-               'WikiImporter' => 'includes/SpecialImport.php',
-               'WikiRevision' => 'includes/SpecialImport.php',
                'WikiXmlError' => 'includes/WikiError.php',
-               'WithoutInterwikiPage' => 'includes/SpecialWithoutinterwiki.php',
                'WordLevelDiff' => 'includes/DifferenceEngine.php',
                'XCacheBagOStuff' => 'includes/BagOStuff.php',
                'XmlDumpWriter' => 'includes/Export.php',
@@ -296,46 +213,11 @@ function __autoload($className) {
                'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
                'ZhClient' => 'includes/ZhClient.php',
 
-               # filerepo
-               'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
-               'File' => 'includes/filerepo/File.php',
-               'FileRepo' => 'includes/filerepo/FileRepo.php',
-               'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
-               'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
-               'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
-               'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
-               'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
-               'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
-               'FSRepo' => 'includes/filerepo/FSRepo.php',
-               'Image' => 'includes/filerepo/Image.php',
-               'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
-               'LocalFile' => 'includes/filerepo/LocalFile.php',
-               'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
-               'LocalRepo' => 'includes/filerepo/LocalRepo.php',
-               'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
-               'RepoGroup' => 'includes/filerepo/RepoGroup.php',
-               'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
-
-               # Media
-               'BitmapHandler' => 'includes/media/Bitmap.php',
-               'BmpHandler' => 'includes/media/BMP.php',
-               'DjVuHandler' => 'includes/media/DjVu.php',
-               'ImageHandler' => 'includes/media/Generic.php',
-               'MediaHandler' => 'includes/media/Generic.php',
-               'SvgHandler' => 'includes/media/SVG.php',
-
-               # Normal
-               'UtfNormal' => 'includes/normal/UtfNormal.php',
-
-               # Templates
-               'UsercreateTemplate' => 'includes/templates/Userlogin.php',
-               'UserloginTemplate' => 'includes/templates/Userlogin.php',
-
-               # Languages
-               'Language' => 'languages/Language.php',
-
-               # API
+               # includes/api
                'ApiBase' => 'includes/api/ApiBase.php',
+               'ApiBlock' => 'includes/api/ApiBlock.php',
+               'ApiDelete' => 'includes/api/ApiDelete.php',
+               'ApiEditPage' => 'includes/api/ApiEditPage.php',
                'ApiEmailUser' => 'includes/api/ApiEmailUser.php',
                'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php',
                'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php',
@@ -352,28 +234,32 @@ function __autoload($className) {
                'ApiLogin' => 'includes/api/ApiLogin.php',
                'ApiLogout' => 'includes/api/ApiLogout.php',
                'ApiMain' => 'includes/api/ApiMain.php',
+               'ApiMove' => 'includes/api/ApiMove.php',
                'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
                'ApiPageSet' => 'includes/api/ApiPageSet.php',
                'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
                'ApiParse' => 'includes/api/ApiParse.php',
-               'ApiQueryAllImages' => 'includes/api/ApiQueryAllimages.php',
+               'ApiProtect' => 'includes/api/ApiProtect.php',
+               'ApiQuery' => 'includes/api/ApiQuery.php',
                'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
+               'ApiQueryAllimages' => 'includes/api/ApiQueryAllimages.php',
                'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
+               'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
                'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
                'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
-               'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
                'ApiQueryBacklinks' => 'includes/api/ApiQueryBacklinks.php',
                'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
+               'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
                'ApiQueryCategories' => 'includes/api/ApiQueryCategories.php',
-               'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
                'ApiQueryCategoryInfo' => 'includes/api/ApiQueryCategoryInfo.php',
+               'ApiQueryCategoryMembers' => 'includes/api/ApiQueryCategoryMembers.php',
                'ApiQueryContributions' => 'includes/api/ApiQueryUserContributions.php',
-               'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
+               'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
                'ApiQueryExtLinksUsage' => 'includes/api/ApiQueryExtLinksUsage.php',
+               'ApiQueryExternalLinks' => 'includes/api/ApiQueryExternalLinks.php',
                'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php',
                'ApiQueryImageInfo' => 'includes/api/ApiQueryImageInfo.php',
                'ApiQueryImages' => 'includes/api/ApiQueryImages.php',
-               'ApiQuery' => 'includes/api/ApiQuery.php',
                'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php',
                'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
                'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
@@ -387,67 +273,247 @@ function __autoload($className) {
                'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
                'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
                'ApiResult' => 'includes/api/ApiResult.php',
-               'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
-               'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
-
-               # apiedit branch
-               'ApiBlock' => 'includes/api/ApiBlock.php',
-               'ApiDelete' => 'includes/api/ApiDelete.php',
-               'ApiEditPage' => 'includes/api/ApiEditPage.php',
-               'ApiMove' => 'includes/api/ApiMove.php',
-               'ApiProtect' => 'includes/api/ApiProtect.php',
-               'ApiQueryBlocks' => 'includes/api/ApiQueryBlocks.php',
-               'ApiQueryDeletedrevs' => 'includes/api/ApiQueryDeletedrevs.php',
                'ApiRollback' => 'includes/api/ApiRollback.php',
                'ApiUnblock' => 'includes/api/ApiUnblock.php',
                'ApiUndelete' => 'includes/api/ApiUndelete.php',
+               'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
+               'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php',
+               'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
+               'UsageException' => 'includes/api/ApiMain.php',
+               'YAMLNode' => 'includes/api/ApiFormatYaml_spyc.php',
+
+               # includes/db
+               'Blob' => 'includes/db/Database.php',
+               'ChronologyProtector' => 'includes/db/LBFactory.php',
+               'Database' => 'includes/db/Database.php',
+               'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
+               'DatabaseMysql' => 'includes/db/Database.php',
+               'DatabaseOracle' => 'includes/db/DatabaseOracle.php',
+               'DatabasePostgres' => 'includes/db/DatabasePostgres.php',
+               'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
+               'DBConnectionError' => 'includes/db/Database.php',
+               'DBError' => 'includes/db/Database.php',
+               'DBObject' => 'includes/db/Database.php',
+               'DBQueryError' => 'includes/db/Database.php',
+               'DBUnexpectedError' => 'includes/db/Database.php',
+               'LBFactory' => 'includes/db/LBFactory.php',
+               'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
+               'LBFactory_Simple' => 'includes/db/LBFactory.php',
+               'LoadBalancer' => 'includes/db/LoadBalancer.php',
+               'MSSQLField' => 'includes/db/DatabaseMssql.php',
+               'MySQLField' => 'includes/db/Database.php',
+               'MySQLMasterPos' => 'includes/db/Database.php',
+               'ORABlob' => 'includes/db/DatabaseOracle.php',
+               'ORAResult' => 'includes/db/DatabaseOracle.php',
+               'PostgresField' => 'includes/db/DatabasePostgres.php',
+               'ResultWrapper' => 'includes/db/Database.php',
+               'SQLiteField' => 'includes/db/DatabaseSqlite.php',
+
+               # includes/filerepo
+               'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
+               'File' => 'includes/filerepo/File.php',
+               'FileRepo' => 'includes/filerepo/FileRepo.php',
+               'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
+               'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
+               'ForeignAPIRepo' => 'includes/filerepo/ForeignAPIRepo.php',
+               'ForeignDBFile' => 'includes/filerepo/ForeignDBFile.php',
+               'ForeignDBRepo' => 'includes/filerepo/ForeignDBRepo.php',
+               'ForeignDBViaLBRepo' => 'includes/filerepo/ForeignDBViaLBRepo.php',
+               'FSRepo' => 'includes/filerepo/FSRepo.php',
+               'Image' => 'includes/filerepo/Image.php',
+               'LocalFile' => 'includes/filerepo/LocalFile.php',
+               'LocalFileDeleteBatch' => 'includes/filerepo/LocalFile.php',
+               'LocalFileMoveBatch' => 'includes/filerepo/LocalFile.php',
+               'LocalFileRestoreBatch' => 'includes/filerepo/LocalFile.php',
+               'LocalRepo' => 'includes/filerepo/LocalRepo.php',
+               'OldLocalFile' => 'includes/filerepo/OldLocalFile.php',
+               'RepoGroup' => 'includes/filerepo/RepoGroup.php',
+               'UnregisteredLocalFile' => 'includes/filerepo/UnregisteredLocalFile.php',
+
+               # includes/media
+               'BitmapHandler' => 'includes/media/Bitmap.php',
+               'BmpHandler' => 'includes/media/BMP.php',
+               'DjVuHandler' => 'includes/media/DjVu.php',
+               'ImageHandler' => 'includes/media/Generic.php',
+               'MediaHandler' => 'includes/media/Generic.php',
+               'SvgHandler' => 'includes/media/SVG.php',
+
+               # includes/normal
+               'UtfNormal' => 'includes/normal/UtfNormal.php',
+
+               # includes/parser
+               'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php',
+               'DateFormatter' => 'includes/parser/DateFormatter.php',
+               'OnlyIncludeReplacer' => 'includes/parser/Parser.php',
+               'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'PPDPart' => 'includes/parser/Preprocessor_DOM.php',
+               'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'PPDStack' => 'includes/parser/Preprocessor_DOM.php',
+               'PPDStackElement' => 'includes/parser/Preprocessor_DOM.php',
+               'PPDStackElement_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'PPDStack_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'PPFrame' => 'includes/parser/Preprocessor.php',
+               'PPFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
+               'PPFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'PPNode' => 'includes/parser/Preprocessor.php',
+               'PPNode_DOM' => 'includes/parser/Preprocessor_DOM.php',
+               'PPNode_Hash_Array' => 'includes/parser/Preprocessor_Hash.php',
+               'PPNode_Hash_Attr' => 'includes/parser/Preprocessor_Hash.php',
+               'PPNode_Hash_Text' => 'includes/parser/Preprocessor_Hash.php',
+               'PPNode_Hash_Tree' => 'includes/parser/Preprocessor_Hash.php',
+               'PPTemplateFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
+               'PPTemplateFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'Parser' => 'includes/parser/Parser.php',
+               'ParserCache' => 'includes/parser/ParserCache.php',
+               'ParserOptions' => 'includes/parser/ParserOptions.php',
+               'ParserOutput' => 'includes/parser/ParserOutput.php',
+               'Parser_DiffTest' => 'includes/parser/Parser_DiffTest.php',
+               'Parser_OldPP' => 'includes/parser/Parser_OldPP.php',
+               'Preprocessor' => 'includes/parser/Preprocessor.php',
+               'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php',
+               'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php',
+               'StripState' => 'includes/parser/Parser.php',
+
+               # includes/specials
+               'AncientPagesPage' => 'includes/specials/Ancientpages.php',
+               'BrokenRedirectsPage' => 'includes/specials/BrokenRedirects.php',
+               'ContribsPager' => 'includes/specials/Contributions.php',
+               'DBLockForm' => 'includes/specials/Lockdb.php',
+               'DBUnlockForm' => 'includes/specials/Unlockdb.php',
+               'DeadendPagesPage' => 'includes/specials/Deadendpages.php',
+               'DisambiguationsPage' => 'includes/specials/Disambiguations.php',
+               'DoubleRedirectsPage' => 'includes/specials/DoubleRedirects.php',
+               'EmailConfirmation' => 'includes/specials/Confirmemail.php',
+               'EmailInvalidation' => 'includes/specials/Confirmemail.php',
+               'EmailUserForm' => 'includes/specials/Emailuser.php',
+               'FewestrevisionsPage' => 'includes/specials/Fewestrevisions.php',
+               'FileDuplicateSearchPage' => 'includes/specials/FileDuplicateSearch.php',
+               'IPBlockForm' => 'includes/specials/Blockip.php',
+               'IPBlocklistPager' => 'includes/specials/Ipblocklist.php',
+               'IPUnblockForm' => 'includes/specials/Ipblocklist.php',
+               'ImportReporter' => 'includes/specials/Import.php',
+               'ImportStreamSource' => 'includes/specials/Import.php',
+               'ImportStringSource' => 'includes/specials/Import.php',
+               'ListredirectsPage' => 'includes/specials/Listredirects.php',
+               'LoginForm' => 'includes/specials/Userlogin.php',
+               'LonelyPagesPage' => 'includes/specials/Lonelypages.php',
+               'LongPagesPage' => 'includes/specials/Longpages.php',
+               'MIMEsearchPage' => 'includes/specials/MIMEsearch.php',
+               'MostcategoriesPage' => 'includes/specials/Mostcategories.php',
+               'MostimagesPage' => 'includes/specials/Mostimages.php',
+               'MostlinkedCategoriesPage' => 'includes/specials/Mostlinkedcategories.php',
+               'MostlinkedPage' => 'includes/specials/Mostlinked.php',
+               'MostrevisionsPage' => 'includes/specials/Mostrevisions.php',
+               'MovePageForm' => 'includes/specials/Movepage.php',
+               'NewPagesForm' => 'includes/specials/Newpages.php',
+               'NewPagesPager' => 'includes/specials/Newpages.php',
+               'PageArchive' => 'includes/specials/Undelete.php',
+               'PasswordResetForm' => 'includes/specials/Resetpass.php',
+               'PopularPagesPage' => 'includes/specials/Popularpages.php',
+               'PreferencesForm' => 'includes/specials/Preferences.php',
+               'RandomPage' => 'includes/specials/Randompage.php',
+               'RevisionDeleteForm' => 'includes/specials/Revisiondelete.php',
+               'RevisionDeleter' => 'includes/specials/Revisiondelete.php',
+               'ShortPagesPage' => 'includes/specials/Shortpages.php',
+               'SpecialAllpages' => 'includes/specials/Allpages.php',
+               'SpecialBookSources' => 'includes/specials/Booksources.php',
+               'SpecialListGroupRights' => 'includes/specials/Listgrouprights.php',
+               'SpecialMostlinkedtemplates' => 'includes/specials/Mostlinkedtemplates.php',
+               'SpecialPrefixindex' => 'includes/specials/Prefixindex.php',
+               'SpecialRandomredirect' => 'includes/specials/Randomredirect.php',
+               'SpecialSearch' => 'includes/specials/Search.php',
+               'SpecialVersion' => 'includes/specials/Version.php',
+               'UncategorizedCategoriesPage' => 'includes/specials/Uncategorizedcategories.php',
+               'UncategorizedPagesPage' => 'includes/specials/Uncategorizedpages.php',
+               'UncategorizedTemplatesPage' => 'includes/specials/Uncategorizedtemplates.php',
+               'UndeleteForm' => 'includes/specials/Undelete.php',
+               'UnusedCategoriesPage' => 'includes/specials/Unusedcategories.php',
+               'UnusedimagesPage' => 'includes/specials/Unusedimages.php',
+               'UnusedtemplatesPage' => 'includes/specials/Unusedtemplates.php',
+               'UnwatchedpagesPage' => 'includes/specials/Unwatchedpages.php',
+               'UploadForm' => 'includes/specials/Upload.php',
+               'UploadFormMogile' => 'includes/specials/UploadMogile.php',
+               'UserrightsPage' => 'includes/specials/Userrights.php',
+               'UsersPager' => 'includes/specials/Listusers.php',
+               'WantedCategoriesPage' => 'includes/specials/Wantedcategories.php',
+               'WantedPagesPage' => 'includes/specials/Wantedpages.php',
+               'WhatLinksHerePage' => 'includes/specials/Whatlinkshere.php',
+               'WikiImporter' => 'includes/specials/Import.php',
+               'WikiRevision' => 'includes/specials/Import.php',
+               'WithoutInterwikiPage' => 'includes/specials/Withoutinterwiki.php',
+
+               # includes/templates
+               'UsercreateTemplate' => 'includes/templates/Userlogin.php',
+               'UserloginTemplate' => 'includes/templates/Userlogin.php',
+
+               # languages
+               'Language' => 'languages/Language.php',
+               'FakeConverter' => 'languages/Language.php',
+
        );
 
-       wfProfileIn( __METHOD__ );
-       if ( isset( $localClasses[$className] ) ) {
-               $filename = $localClasses[$className];
-       } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
-               $filename = $wgAutoloadClasses[$className];
-       } else {
-               # Try a different capitalisation
-               # The case can sometimes be wrong when unserializing PHP 4 objects
-               $filename = false;
-               $lowerClass = strtolower( $className );
-               foreach ( $localClasses as $class2 => $file2 ) {
-                       if ( strtolower( $class2 ) == $lowerClass ) {
-                               $filename = $file2;
+       static function autoload( $className ) {
+               global $wgAutoloadClasses;
+
+               wfProfileIn( __METHOD__ );
+               if ( isset( self::$localClasses[$className] ) ) {
+                       $filename = self::$localClasses[$className];
+               } elseif ( isset( $wgAutoloadClasses[$className] ) ) {
+                       $filename = $wgAutoloadClasses[$className];
+               } else {
+                       # Try a different capitalisation
+                       # The case can sometimes be wrong when unserializing PHP 4 objects
+                       $filename = false;
+                       $lowerClass = strtolower( $className );
+                       foreach ( self::$localClasses as $class2 => $file2 ) {
+                               if ( strtolower( $class2 ) == $lowerClass ) {
+                                       $filename = $file2;
+                               }
+                       }
+                       if ( !$filename ) {
+                               # Give up
+                               wfProfileOut( __METHOD__ );
+                               return;
                        }
                }
-               if ( !$filename ) {
-                       # Give up
-                       wfProfileOut( __METHOD__ );
-                       return;
+
+               # Make an absolute path, this improves performance by avoiding some stat calls
+               if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) {
+                       global $IP;
+                       $filename = "$IP/$filename";
                }
+               require( $filename );
+               wfProfileOut( __METHOD__ );
        }
 
-       # Make an absolute path, this improves performance by avoiding some stat calls
-       if ( substr( $filename, 0, 1 ) != '/' && substr( $filename, 1, 1 ) != ':' ) {
-               global $IP;
-               $filename = "$IP/$filename";
+       static function loadAllExtensions() {
+               global $wgAutoloadClasses;
+
+               # It is crucial that SpecialPage.php is included before any special page
+               # extensions are loaded. Otherwise the parent class will not be available
+               # when APC loads the early-bound extension class. Normally this is
+               # guaranteed by entering special pages via SpecialPage members such as
+               # executePath(), but here we have to take a more explicit measure.
+
+               require_once( dirname(__FILE__) . '/SpecialPage.php' );
+
+               foreach( $wgAutoloadClasses as $class => $file ) {
+                       if( !( class_exists( $class ) || interface_exists( $class ) ) ) {
+                               require( $file );
+                       }
+               }
        }
-       require( $filename );
-       wfProfileOut( __METHOD__ );
 }
 
 function wfLoadAllExtensions() {
-       global $wgAutoloadClasses;
-
-       # It is crucial that SpecialPage.php is included before any special page
-       # extensions are loaded. Otherwise the parent class will not be available
-       # when APC loads the early-bound extension class. Normally this is
-       # guaranteed by entering special pages via SpecialPage members such as
-       # executePath(), but here we have to take a more explicit measure.
-
-       require_once( dirname(__FILE__) . '/SpecialPage.php' );
+       AutoLoader::loadAllExtensions();
+}
 
-       foreach( $wgAutoloadClasses as $class => $file ) {
-               if( !( class_exists( $class ) || interface_exists( $class ) ) ) {
-                       require( $file );
-               }
+if ( function_exists( 'spl_autoload_register' ) ) {
+       spl_autoload_register( array( 'AutoLoader', 'autoload' ) );
+} else {
+       function __autoload( $class ) {
+               AutoLoader::autoload( $class );
        }
 }
+
diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php
deleted file mode 100644 (file)
index d9072e9..0000000
+++ /dev/null
@@ -1,385 +0,0 @@
-<?php
-
-/**
- * Various core parser functions, registered in Parser::firstCallInit()
- * @ingroup Parser
- */
-class CoreParserFunctions {
-       static function register( $parser ) {
-               global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
-
-               # Syntax for arguments (see self::setFunctionHook):
-               #  "name for lookup in localized magic words array",
-               #  function callback,
-               #  optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}
-               #    instead of {{#int:...}})
-
-               $parser->setFunctionHook( 'int',              array( __CLASS__, 'intFunction'      ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'ns',               array( __CLASS__, 'ns'               ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'urlencode',        array( __CLASS__, 'urlencode'        ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'lcfirst',          array( __CLASS__, 'lcfirst'          ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'ucfirst',          array( __CLASS__, 'ucfirst'          ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'lc',               array( __CLASS__, 'lc'               ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'uc',               array( __CLASS__, 'uc'               ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'localurl',         array( __CLASS__, 'localurl'         ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'localurle',        array( __CLASS__, 'localurle'        ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'fullurl',          array( __CLASS__, 'fullurl'          ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'fullurle',         array( __CLASS__, 'fullurle'         ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'formatnum',        array( __CLASS__, 'formatnum'        ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'grammar',          array( __CLASS__, 'grammar'          ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'plural',           array( __CLASS__, 'plural'           ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'numberofpages',    array( __CLASS__, 'numberofpages'    ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'numberofusers',    array( __CLASS__, 'numberofusers'    ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'numberoffiles',    array( __CLASS__, 'numberoffiles'    ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'numberofadmins',   array( __CLASS__, 'numberofadmins'   ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'numberofedits',    array( __CLASS__, 'numberofedits'    ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'language',         array( __CLASS__, 'language'         ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'padleft',          array( __CLASS__, 'padleft'          ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'padright',         array( __CLASS__, 'padright'         ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'anchorencode',     array( __CLASS__, 'anchorencode'     ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'special',          array( __CLASS__, 'special'          ) );
-               $parser->setFunctionHook( 'defaultsort',      array( __CLASS__, 'defaultsort'      ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'filepath',         array( __CLASS__, 'filepath'         ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'pagesincategory',  array( __CLASS__, 'pagesincategory'  ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'pagesize',         array( __CLASS__, 'pagesize'         ), SFH_NO_HASH );
-               $parser->setFunctionHook( 'tag',              array( __CLASS__, 'tagObj'           ), SFH_OBJECT_ARGS );
-
-               if ( $wgAllowDisplayTitle ) {
-                       $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
-               }
-               if ( $wgAllowSlowParserFunctions ) {
-                       $parser->setFunctionHook( 'pagesinnamespace', array( __CLASS__, 'pagesinnamespace' ), SFH_NO_HASH );
-               }
-       }
-
-       static function intFunction( $parser, $part1 = '' /*, ... */ ) {
-               if ( strval( $part1 ) !== '' ) {
-                       $args = array_slice( func_get_args(), 2 );
-                       return wfMsgReal( $part1, $args, true );
-               } else {
-                       return array( 'found' => false );
-               }
-       }
-
-       static function ns( $parser, $part1 = '' ) {
-               global $wgContLang;
-               $found = false;
-               if ( intval( $part1 ) || $part1 == "0" ) {
-                       $text = $wgContLang->getNsText( intval( $part1 ) );
-                       $found = true;
-               } else {
-                       $param = str_replace( ' ', '_', strtolower( $part1 ) );
-                       $index = MWNamespace::getCanonicalIndex( strtolower( $param ) );
-                       if ( !is_null( $index ) ) {
-                               $text = $wgContLang->getNsText( $index );
-                               $found = true;
-                       }
-               }
-               if ( $found ) {
-                       return $text;
-               } else {
-                       return array( 'found' => false );
-               }
-       }
-
-       static function urlencode( $parser, $s = '' ) {
-               return urlencode( $s );
-       }
-
-       static function lcfirst( $parser, $s = '' ) {
-               global $wgContLang;
-               return $wgContLang->lcfirst( $s );
-       }
-
-       static function ucfirst( $parser, $s = '' ) {
-               global $wgContLang;
-               return $wgContLang->ucfirst( $s );
-       }
-
-       static function lc( $parser, $s = '' ) {
-               global $wgContLang;
-               if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
-                       return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) );
-               } else {
-                       return $wgContLang->lc( $s );
-               }
-       }
-
-       static function uc( $parser, $s = '' ) {
-               global $wgContLang;
-               if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
-                       return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) );
-               } else {
-                       return $wgContLang->uc( $s );
-               }
-       }
-
-       static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); }
-       static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); }
-       static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); }
-       static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); }
-
-       static function urlFunction( $func, $s = '', $arg = null ) {
-               $title = Title::newFromText( $s );
-               # Due to order of execution of a lot of bits, the values might be encoded
-               # before arriving here; if that's true, then the title can't be created
-               # and the variable will fail. If we can't get a decent title from the first
-               # attempt, url-decode and try for a second.
-               if( is_null( $title ) )
-                       $title = Title::newFromUrl( urldecode( $s ) );
-               if ( !is_null( $title ) ) {
-                       if ( !is_null( $arg ) ) {
-                               $text = $title->$func( $arg );
-                       } else {
-                               $text = $title->$func();
-                       }
-                       return $text;
-               } else {
-                       return array( 'found' => false );
-               }
-       }
-
-       static function formatNum( $parser, $num = '', $raw = null) {
-               if ( self::israw( $raw ) ) {
-                       return $parser->getFunctionLang()->parseFormattedNumber( $num );
-               } else {
-                       return $parser->getFunctionLang()->formatNum( $num );
-               }
-       }
-
-       static function grammar( $parser, $case = '', $word = '' ) {
-               return $parser->getFunctionLang()->convertGrammar( $word, $case );
-       }
-
-       static function plural( $parser, $text = '') {
-               $forms = array_slice( func_get_args(), 2);
-               $text = $parser->getFunctionLang()->parseFormattedNumber( $text );
-               return $parser->getFunctionLang()->convertPlural( $text, $forms );
-       }
-
-       /**
-        * Override the title of the page when viewed, provided we've been given a
-        * title which will normalise to the canonical title
-        *
-        * @param Parser $parser Parent parser
-        * @param string $text Desired title text
-        * @return string
-        */
-       static function displaytitle( $parser, $text = '' ) {
-               $text = trim( Sanitizer::decodeCharReferences( $text ) );
-               $title = Title::newFromText( $text );
-               if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
-                       $parser->mOutput->setDisplayTitle( $text );
-               return '';
-       }
-
-       static function isRaw( $param ) {
-               static $mwRaw;
-               if ( !$mwRaw ) {
-                       $mwRaw =& MagicWord::get( 'rawsuffix' );
-               }
-               if ( is_null( $param ) ) {
-                       return false;
-               } else {
-                       return $mwRaw->match( $param );
-               }
-       }
-
-       static function formatRaw( $num, $raw ) {
-               if( self::isRaw( $raw ) ) {
-                       return $num;
-               } else {
-                       global $wgContLang;
-                       return $wgContLang->formatNum( $num );
-               }
-       }
-       static function numberofpages( $parser, $raw = null ) {
-               return self::formatRaw( SiteStats::pages(), $raw );
-       }
-       static function numberofusers( $parser, $raw = null ) {
-               return self::formatRaw( SiteStats::users(), $raw );
-       }
-       static function numberofarticles( $parser, $raw = null ) {
-               return self::formatRaw( SiteStats::articles(), $raw );
-       }
-       static function numberoffiles( $parser, $raw = null ) {
-               return self::formatRaw( SiteStats::images(), $raw );
-       }
-       static function numberofadmins( $parser, $raw = null ) {
-               return self::formatRaw( SiteStats::admins(), $raw );
-       }
-       static function numberofedits( $parser, $raw = null ) {
-               return self::formatRaw( SiteStats::edits(), $raw );
-       }
-       static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
-               return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
-       }
-
-       /**
-        * Return the number of pages in the given category, or 0 if it's nonexis-
-        * tent.  This is an expensive parser function and can't be called too many
-        * times per page.
-        */
-       static function pagesincategory( $parser, $name = '', $raw = null ) {
-               static $cache = array();
-               $category = Category::newFromName( $name );
-
-               if( !is_object( $category ) ) {
-                       $cache[$name] = 0;
-                       return self::formatRaw( 0, $raw );
-               }
-
-               # Normalize name for cache
-               $name = $category->getName();
-
-               $count = 0;
-               if( isset( $cache[$name] ) ) {
-                       $count = $cache[$name];
-               } elseif( $parser->incrementExpensiveFunctionCount() ) {
-                       $count = $cache[$name] = (int)$category->getPageCount();
-               }
-               return self::formatRaw( $count, $raw );
-       }
-
-       /**
-        * Return the size of the given page, or 0 if it's nonexistent.  This is an
-        * expensive parser function and can't be called too many times per page.
-        *
-        * @FIXME This doesn't work correctly on preview for getting the size of
-        *   the current page.
-        * @FIXME Title::getLength() documentation claims that it adds things to
-        *   the link cache, so the local cache here should be unnecessary, but in
-        *   fact calling getLength() repeatedly for the same $page does seem to
-        *   run one query for each call?
-        */
-       static function pagesize( $parser, $page = '', $raw = null ) {
-               static $cache = array();
-               $title = Title::newFromText($page);
-
-               if( !is_object( $title ) ) {
-                       $cache[$page] = 0;
-                       return self::formatRaw( 0, $raw );
-               }
-
-               # Normalize name for cache
-               $page = $title->getPrefixedText();
-
-               $length = 0;
-               if( isset( $cache[$page] ) ) {
-                       $length = $cache[$page];
-               } elseif( $parser->incrementExpensiveFunctionCount() ) {
-                       $length = $cache[$page] = $title->getLength();
-       
-                       // Register dependency in templatelinks
-                       $id = $title->getArticleId();
-                       $revid = Revision::newFromTitle($title);
-                       $parser->mOutput->addTemplate($title, $id, $revid);
-               }       
-               return self::formatRaw( $length, $raw );
-       }
-
-       static function language( $parser, $arg = '' ) {
-               global $wgContLang;
-               $lang = $wgContLang->getLanguageName( strtolower( $arg ) );
-               return $lang != '' ? $lang : $arg;
-       }
-
-       static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) {
-               $length = min( max( $length, 0 ), 500 );
-               $char = substr( $char, 0, 1 );
-               return ( $string !== '' && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 )
-                               ? str_pad( $string, $length, (string)$char, $direction )
-                               : $string;
-       }
-
-       static function padleft( $parser, $string = '', $length = 0, $char = 0 ) {
-               return self::pad( $string, $length, $char, STR_PAD_LEFT );
-       }
-
-       static function padright( $parser, $string = '', $length = 0, $char = 0 ) {
-               return self::pad( $string, $length, $char );
-       }
-
-       static function anchorencode( $parser, $text ) {
-               $a = urlencode( $text );
-               $a = strtr( $a, array( '%' => '.', '+' => '_' ) );
-               # leave colons alone, however
-               $a = str_replace( '.3A', ':', $a );
-               return $a;
-       }
-
-       static function special( $parser, $text ) {
-               $title = SpecialPage::getTitleForAlias( $text );
-               if ( $title ) {
-                       return $title->getPrefixedText();
-               } else {
-                       return wfMsgForContent( 'nosuchspecialpage' );
-               }
-       }
-
-       public static function defaultsort( $parser, $text ) {
-               $text = trim( $text );
-               if( strlen( $text ) > 0 )
-                       $parser->setDefaultSort( $text );
-               return '';
-       }
-
-       public static function filepath( $parser, $name='', $option='' ) {
-               $file = wfFindFile( $name );
-               if( $file ) {
-                       $url = $file->getFullUrl();
-                       if( $option == 'nowiki' ) {
-                               return "<nowiki>$url</nowiki>";
-                       }
-                       return $url;
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * Parser function to extension tag adaptor
-        */
-       public static function tagObj( $parser, $frame, $args ) {
-               $xpath = false;
-               if ( !count( $args ) ) {
-                       return '';
-               }
-               $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
-
-               if ( count( $args ) ) {
-                       $inner = $frame->expand( array_shift( $args ) );
-               } else {
-                       $inner = null;
-               }
-
-               $stripList = $parser->getStripList();
-               if ( !in_array( $tagName, $stripList ) ) {
-                       return '<span class="error">' .
-                               wfMsg( 'unknown_extension_tag', $tagName ) .
-                               '</span>';
-               }
-
-               $attributes = array();
-               foreach ( $args as $arg ) {
-                       $bits = $arg->splitArg();
-                       if ( strval( $bits['index'] ) === '' ) {
-                               $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS );
-                               $value = trim( $frame->expand( $bits['value'] ) );
-                               if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
-                                       $value = isset( $m[1] ) ? $m[1] : '';
-                               }
-                               $attributes[$name] = $value;
-                       }
-               }
-
-               $params = array(
-                       'name' => $tagName,
-                       'inner' => $inner,
-                       'attributes' => $attributes,
-                       'close' => "</$tagName>",
-               );
-               return $parser->extensionSubstitution( $params, $frame );
-       }
-}
diff --git a/includes/Database.php b/includes/Database.php
deleted file mode 100644 (file)
index b6a8c8b..0000000
+++ /dev/null
@@ -1,2700 +0,0 @@
-<?php
-/**
- * @defgroup Database Database
- *
- * @file
- * @ingroup Database
- * This file deals with MySQL interface functions
- * and query specifics/optimisations
- */
-
-/** Number of times to re-try an operation in case of deadlock */
-define( 'DEADLOCK_TRIES', 4 );
-/** Minimum time to wait before retry, in microseconds */
-define( 'DEADLOCK_DELAY_MIN', 500000 );
-/** Maximum time to wait before retry */
-define( 'DEADLOCK_DELAY_MAX', 1500000 );
-
-/**
- * Database abstraction object
- * @ingroup Database
- */
-class Database {
-
-#------------------------------------------------------------------------------
-# Variables
-#------------------------------------------------------------------------------
-
-       protected $mLastQuery = '';
-       protected $mPHPError = false;
-
-       protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
-       protected $mOut, $mOpened = false;
-
-       protected $mFailFunction;
-       protected $mTablePrefix;
-       protected $mFlags;
-       protected $mTrxLevel = 0;
-       protected $mErrorCount = 0;
-       protected $mLBInfo = array();
-       protected $mFakeSlaveLag = null, $mFakeMaster = false;
-
-#------------------------------------------------------------------------------
-# Accessors
-#------------------------------------------------------------------------------
-       # These optionally set a variable and return the previous state
-
-       /**
-        * Fail function, takes a Database as a parameter
-        * Set to false for default, 1 for ignore errors
-        */
-       function failFunction( $function = NULL ) {
-               return wfSetVar( $this->mFailFunction, $function );
-       }
-
-       /**
-        * Output page, used for reporting errors
-        * FALSE means discard output
-        */
-       function setOutputPage( $out ) {
-               $this->mOut = $out;
-       }
-
-       /**
-        * Boolean, controls output of large amounts of debug information
-        */
-       function debug( $debug = NULL ) {
-               return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
-       }
-
-       /**
-        * Turns buffering of SQL result sets on (true) or off (false).
-        * Default is "on" and it should not be changed without good reasons.
-        */
-       function bufferResults( $buffer = NULL ) {
-               if ( is_null( $buffer ) ) {
-                       return !(bool)( $this->mFlags & DBO_NOBUFFER );
-               } else {
-                       return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
-               }
-       }
-
-       /**
-        * Turns on (false) or off (true) the automatic generation and sending
-        * of a "we're sorry, but there has been a database error" page on
-        * database errors. Default is on (false). When turned off, the
-        * code should use lastErrno() and lastError() to handle the
-        * situation as appropriate.
-        */
-       function ignoreErrors( $ignoreErrors = NULL ) {
-               return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
-       }
-
-       /**
-        * The current depth of nested transactions
-        * @param $level Integer: , default NULL.
-        */
-       function trxLevel( $level = NULL ) {
-               return wfSetVar( $this->mTrxLevel, $level );
-       }
-
-       /**
-        * Number of errors logged, only useful when errors are ignored
-        */
-       function errorCount( $count = NULL ) {
-               return wfSetVar( $this->mErrorCount, $count );
-       }
-
-       function tablePrefix( $prefix = null ) {
-               return wfSetVar( $this->mTablePrefix, $prefix );
-       }
-
-       /**
-        * Properties passed down from the server info array of the load balancer
-        */
-       function getLBInfo( $name = NULL ) {
-               if ( is_null( $name ) ) {
-                       return $this->mLBInfo;
-               } else {
-                       if ( array_key_exists( $name, $this->mLBInfo ) ) {
-                               return $this->mLBInfo[$name];
-                       } else {
-                               return NULL;
-                       }
-               }
-       }
-
-       function setLBInfo( $name, $value = NULL ) {
-               if ( is_null( $value ) ) {
-                       $this->mLBInfo = $name;
-               } else {
-                       $this->mLBInfo[$name] = $value;
-               }
-       }
-
-       /**
-        * Set lag time in seconds for a fake slave
-        */
-       function setFakeSlaveLag( $lag ) {
-               $this->mFakeSlaveLag = $lag;
-       }
-
-       /**
-        * Make this connection a fake master
-        */
-       function setFakeMaster( $enabled = true ) {
-               $this->mFakeMaster = $enabled;
-       }
-
-       /**
-        * Returns true if this database supports (and uses) cascading deletes
-        */
-       function cascadingDeletes() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database supports (and uses) triggers (e.g. on the page table)
-        */
-       function cleanupTriggers() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database is strict about what can be put into an IP field.
-        * Specifically, it uses a NULL value instead of an empty string.
-        */
-       function strictIPs() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database uses timestamps rather than integers
-       */
-       function realTimestamps() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database does an implicit sort when doing GROUP BY
-        */
-       function implicitGroupby() {
-               return true;
-       }
-
-       /**
-        * Returns true if this database does an implicit order by when the column has an index
-        * For example: SELECT page_title FROM page LIMIT 1
-        */
-       function implicitOrderby() {
-               return true;
-       }
-
-       /**
-        * Returns true if this database can do a native search on IP columns
-        * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
-        */
-       function searchableIPs() {
-               return false;
-       }
-
-       /**
-        * Returns true if this database can use functional indexes
-        */
-       function functionalIndexes() {
-               return false;
-       }
-
-       /**#@+
-        * Get function
-        */
-       function lastQuery() { return $this->mLastQuery; }
-       function isOpen() { return $this->mOpened; }
-       /**#@-*/
-
-       function setFlag( $flag ) {
-               $this->mFlags |= $flag;
-       }
-
-       function clearFlag( $flag ) {
-               $this->mFlags &= ~$flag;
-       }
-
-       function getFlag( $flag ) {
-               return !!($this->mFlags & $flag);
-       }
-
-       /**
-        * General read-only accessor
-        */
-       function getProperty( $name ) {
-               return $this->$name;
-       }
-
-       function getWikiID() {
-               if( $this->mTablePrefix ) {
-                       return "{$this->mDBname}-{$this->mTablePrefix}";
-               } else {
-                       return $this->mDBname;
-               }
-       }
-
-#------------------------------------------------------------------------------
-# Other functions
-#------------------------------------------------------------------------------
-
-       /**@{{
-        * Constructor.
-        * @param string $server database server host
-        * @param string $user database user name
-        * @param string $password database user password
-        * @param string $dbname database name
-        * @param failFunction
-        * @param $flags
-        * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
-        */
-       function __construct( $server = false, $user = false, $password = false, $dbName = false,
-               $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
-
-               global $wgOut, $wgDBprefix, $wgCommandLineMode;
-               # Can't get a reference if it hasn't been set yet
-               if ( !isset( $wgOut ) ) {
-                       $wgOut = NULL;
-               }
-               $this->mOut =& $wgOut;
-
-               $this->mFailFunction = $failFunction;
-               $this->mFlags = $flags;
-
-               if ( $this->mFlags & DBO_DEFAULT ) {
-                       if ( $wgCommandLineMode ) {
-                               $this->mFlags &= ~DBO_TRX;
-                       } else {
-                               $this->mFlags |= DBO_TRX;
-                       }
-               }
-
-               /*
-               // Faster read-only access
-               if ( wfReadOnly() ) {
-                       $this->mFlags |= DBO_PERSISTENT;
-                       $this->mFlags &= ~DBO_TRX;
-               }*/
-
-               /** Get the default table prefix*/
-               if ( $tablePrefix == 'get from global' ) {
-                       $this->mTablePrefix = $wgDBprefix;
-               } else {
-                       $this->mTablePrefix = $tablePrefix;
-               }
-
-               if ( $server ) {
-                       $this->open( $server, $user, $password, $dbName );
-               }
-       }
-
-       /**
-        * @static
-        * @param failFunction
-        * @param $flags
-        */
-       static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
-       {
-               return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
-       }
-
-       /**
-        * Usually aborts on failure
-        * If the failFunction is set to a non-zero integer, returns success
-        */
-       function open( $server, $user, $password, $dbName ) {
-               global $wguname, $wgAllDBsAreLocalhost;
-               wfProfileIn( __METHOD__ );
-
-               # Test for missing mysql.so
-               # First try to load it
-               if (!@extension_loaded('mysql')) {
-                       @dl('mysql.so');
-               }
-
-               # Fail now
-               # Otherwise we get a suppressed fatal error, which is very hard to track down
-               if ( !function_exists( 'mysql_connect' ) ) {
-                       throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
-               }
-
-               # Debugging hack -- fake cluster
-               if ( $wgAllDBsAreLocalhost ) {
-                       $realServer = 'localhost';
-               } else {
-                       $realServer = $server;
-               }
-               $this->close();
-               $this->mServer = $server;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
-
-               $success = false;
-
-               wfProfileIn("dbconnect-$server");
-
-               # Try to connect up to three times
-               # The kernel's default SYN retransmission period is far too slow for us,
-               # so we use a short timeout plus a manual retry.
-               $this->mConn = false;
-               $max = 3;
-               $this->installErrorHandler();
-               for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
-                       if ( $i > 1 ) {
-                               usleep( 1000 );
-                       }
-                       if ( $this->mFlags & DBO_PERSISTENT ) {
-                               $this->mConn = mysql_pconnect( $realServer, $user, $password );
-                       } else {
-                               # Create a new connection...
-                               $this->mConn = mysql_connect( $realServer, $user, $password, true );
-                       }
-                       if ($this->mConn === false) {
-                               #$iplus = $i + 1;
-                               #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); 
-                       }
-               }
-               $phpError = $this->restoreErrorHandler();
-               
-               wfProfileOut("dbconnect-$server");
-
-               if ( $dbName != '' ) {
-                       if ( $this->mConn !== false ) {
-                               $success = @/**/mysql_select_db( $dbName, $this->mConn );
-                               if ( !$success ) {
-                                       $error = "Error selecting database $dbName on server {$this->mServer} " .
-                                               "from client host {$wguname['nodename']}\n";
-                                       wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
-                                       wfDebug( $error );
-                               }
-                       } else {
-                               wfDebug( "DB connection error\n" );
-                               wfDebug( "Server: $server, User: $user, Password: " .
-                                       substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
-                               $success = false;
-                       }
-               } else {
-                       # Delay USE query
-                       $success = (bool)$this->mConn;
-               }
-
-               if ( $success ) {
-                       $version = $this->getServerVersion();
-                       if ( version_compare( $version, '4.1' ) >= 0 ) {
-                               // Tell the server we're communicating with it in UTF-8.
-                               // This may engage various charset conversions.
-                               global $wgDBmysql5;
-                               if( $wgDBmysql5 ) {
-                                       $this->query( 'SET NAMES utf8', __METHOD__ );
-                               }
-                               // Turn off strict mode
-                               $this->query( "SET sql_mode = ''", __METHOD__ );
-                       }
-
-                       // Turn off strict mode if it is on
-               } else {
-                       $this->reportConnectionError( $phpError );
-               }
-
-               $this->mOpened = $success;
-               wfProfileOut( __METHOD__ );
-               return $success;
-       }
-       /**@}}*/
-
-       protected function installErrorHandler() {
-               $this->mPHPError = false;
-               set_error_handler( array( $this, 'connectionErrorHandler' ) );
-       }
-
-       protected function restoreErrorHandler() {
-               restore_error_handler();
-               return $this->mPHPError;
-       }
-
-       protected function connectionErrorHandler( $errno,  $errstr ) {
-               $this->mPHPError = $errstr;
-       }
-
-       /**
-        * Closes a database connection.
-        * if it is open : commits any open transactions
-        *
-        * @return bool operation success. true if already closed.
-        */
-       function close()
-       {
-               $this->mOpened = false;
-               if ( $this->mConn ) {
-                       if ( $this->trxLevel() ) {
-                               $this->immediateCommit();
-                       }
-                       return mysql_close( $this->mConn );
-               } else {
-                       return true;
-               }
-       }
-
-       /**
-        * @param string $error fallback error message, used if none is given by MySQL
-        */
-       function reportConnectionError( $error = 'Unknown error' ) {
-               $myError = $this->lastError();
-               if ( $myError ) {
-                       $error = $myError;
-               }
-
-               if ( $this->mFailFunction ) {
-                       # Legacy error handling method
-                       if ( !is_int( $this->mFailFunction ) ) {
-                               $ff = $this->mFailFunction;
-                               $ff( $this, $error );
-                       }
-               } else {
-                       # New method
-                       wfLogDBError( "Connection error: $error\n" );
-                       throw new DBConnectionError( $this, $error );
-               }
-       }
-
-       /**
-        * Usually aborts on failure.  If errors are explicitly ignored, returns success.
-        *
-        * @param  $sql        String: SQL query
-        * @param  $fname      String: Name of the calling function, for profiling/SHOW PROCESSLIST 
-        *     comment (you can use __METHOD__ or add some extra info)
-        * @param  $tempIgnore Bool:   Whether to avoid throwing an exception on errors... 
-        *     maybe best to catch the exception instead?
-        * @return true for a successful write query, ResultWrapper object for a successful read query, 
-        *     or false on failure if $tempIgnore set
-        * @throws DBQueryError Thrown when the database returns an error of any kind
-        */
-       public function query( $sql, $fname = '', $tempIgnore = false ) {
-               global $wgProfiler;
-
-               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
-               if ( isset( $wgProfiler ) ) {
-                       # generalizeSQL will probably cut down the query to reasonable
-                       # logging size most of the time. The substr is really just a sanity check.
-
-                       # Who's been wasting my precious column space? -- TS
-                       #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
-
-                       if ( $isMaster ) {
-                               $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
-                               $totalProf = 'Database::query-master';
-                       } else {
-                               $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
-                               $totalProf = 'Database::query';
-                       }
-                       wfProfileIn( $totalProf );
-                       wfProfileIn( $queryProf );
-               }
-
-               $this->mLastQuery = $sql;
-
-               # Add a comment for easy SHOW PROCESSLIST interpretation
-               #if ( $fname ) {
-                       global $wgUser;
-                       if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
-                               $userName = $wgUser->getName();
-                               if ( mb_strlen( $userName ) > 15 ) {
-                                       $userName = mb_substr( $userName, 0, 15 ) . '...';
-                               }
-                               $userName = str_replace( '/', '', $userName );
-                       } else {
-                               $userName = '';
-                       }
-                       $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
-               #} else {
-               #       $commentedSql = $sql;
-               #}
-
-               # If DBO_TRX is set, start a transaction
-               if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && 
-                       $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
-                       // avoid establishing transactions for SHOW and SET statements too -
-                       // that would delay transaction initializations to once connection 
-                       // is really used by application
-                       $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
-                       if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0) 
-                               $this->begin(); 
-               }
-
-               if ( $this->debug() ) {
-                       $sqlx = substr( $commentedSql, 0, 500 );
-                       $sqlx = strtr( $sqlx, "\t\n", '  ' );
-                       if ( $isMaster ) {
-                               wfDebug( "SQL-master: $sqlx\n" );
-                       } else {
-                               wfDebug( "SQL: $sqlx\n" );
-                       }
-               }
-
-               # Do the query and handle errors
-               $ret = $this->doQuery( $commentedSql );
-
-               # Try reconnecting if the connection was lost
-               if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
-                       # Transaction is gone, like it or not
-                       $this->mTrxLevel = 0;
-                       wfDebug( "Connection lost, reconnecting...\n" );
-                       if ( $this->ping() ) {
-                               wfDebug( "Reconnected\n" );
-                               $sqlx = substr( $commentedSql, 0, 500 );
-                               $sqlx = strtr( $sqlx, "\t\n", '  ' );
-                               global $wgRequestTime;
-                               $elapsed = round( microtime(true) - $wgRequestTime, 3 );
-                               wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
-                               $ret = $this->doQuery( $commentedSql );
-                       } else {
-                               wfDebug( "Failed\n" );
-                       }
-               }
-
-               if ( false === $ret ) {
-                       $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
-               }
-
-               if ( isset( $wgProfiler ) ) {
-                       wfProfileOut( $queryProf );
-                       wfProfileOut( $totalProf );
-               }
-               return $this->resultObject( $ret );
-       }
-
-       /**
-        * The DBMS-dependent part of query()
-        * @param  $sql String: SQL query.
-        * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
-        * @access private
-        */
-       /*private*/ function doQuery( $sql ) {
-               if( $this->bufferResults() ) {
-                       $ret = mysql_query( $sql, $this->mConn );
-               } else {
-                       $ret = mysql_unbuffered_query( $sql, $this->mConn );
-               }
-               return $ret;
-       }
-
-       /**
-        * @param $error
-        * @param $errno
-        * @param $sql
-        * @param string $fname
-        * @param bool $tempIgnore
-        */
-       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               global $wgCommandLineMode;
-               # Ignore errors during error handling to avoid infinite recursion
-               $ignore = $this->ignoreErrors( true );
-               ++$this->mErrorCount;
-
-               if( $ignore || $tempIgnore ) {
-                       wfDebug("SQL ERROR (ignored): $error\n");
-                       $this->ignoreErrors( $ignore );
-               } else {
-                       $sql1line = str_replace( "\n", "\\n", $sql );
-                       wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
-                       wfDebug("SQL ERROR: " . $error . "\n");
-                       throw new DBQueryError( $this, $error, $errno, $sql, $fname );
-               }
-       }
-
-
-       /**
-        * Intended to be compatible with the PEAR::DB wrapper functions.
-        * http://pear.php.net/manual/en/package.database.db.intro-execute.php
-        *
-        * ? = scalar value, quoted as necessary
-        * ! = raw SQL bit (a function for instance)
-        * & = filename; reads the file and inserts as a blob
-        *     (we don't use this though...)
-        */
-       function prepare( $sql, $func = 'Database::prepare' ) {
-               /* MySQL doesn't support prepared statements (yet), so just
-                  pack up the query for reference. We'll manually replace
-                  the bits later. */
-               return array( 'query' => $sql, 'func' => $func );
-       }
-
-       function freePrepared( $prepared ) {
-               /* No-op for MySQL */
-       }
-
-       /**
-        * Execute a prepared query with the various arguments
-        * @param string $prepared the prepared sql
-        * @param mixed $args Either an array here, or put scalars as varargs
-        */
-       function execute( $prepared, $args = null ) {
-               if( !is_array( $args ) ) {
-                       # Pull the var args
-                       $args = func_get_args();
-                       array_shift( $args );
-               }
-               $sql = $this->fillPrepared( $prepared['query'], $args );
-               return $this->query( $sql, $prepared['func'] );
-       }
-
-       /**
-        * Prepare & execute an SQL statement, quoting and inserting arguments
-        * in the appropriate places.
-        * @param string $query
-        * @param string $args ...
-        */
-       function safeQuery( $query, $args = null ) {
-               $prepared = $this->prepare( $query, 'Database::safeQuery' );
-               if( !is_array( $args ) ) {
-                       # Pull the var args
-                       $args = func_get_args();
-                       array_shift( $args );
-               }
-               $retval = $this->execute( $prepared, $args );
-               $this->freePrepared( $prepared );
-               return $retval;
-       }
-
-       /**
-        * For faking prepared SQL statements on DBs that don't support
-        * it directly.
-        * @param string $preparedSql - a 'preparable' SQL statement
-        * @param array $args - array of arguments to fill it with
-        * @return string executable SQL
-        */
-       function fillPrepared( $preparedQuery, $args ) {
-               reset( $args );
-               $this->preparedArgs =& $args;
-               return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
-                       array( &$this, 'fillPreparedArg' ), $preparedQuery );
-       }
-
-       /**
-        * preg_callback func for fillPrepared()
-        * The arguments should be in $this->preparedArgs and must not be touched
-        * while we're doing this.
-        *
-        * @param array $matches
-        * @return string
-        * @private
-        */
-       function fillPreparedArg( $matches ) {
-               switch( $matches[1] ) {
-                       case '\\?': return '?';
-                       case '\\!': return '!';
-                       case '\\&': return '&';
-               }
-               list( /* $n */ , $arg ) = each( $this->preparedArgs );
-               switch( $matches[1] ) {
-                       case '?': return $this->addQuotes( $arg );
-                       case '!': return $arg;
-                       case '&':
-                               # return $this->addQuotes( file_get_contents( $arg ) );
-                               throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
-                       default:
-                               throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
-               }
-       }
-
-       /**#@+
-        * @param mixed $res A SQL result
-        */
-       /**
-        * Free a result object
-        */
-       function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               if ( !@/**/mysql_free_result( $res ) ) {
-                       throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
-               }
-       }
-
-       /**
-        * Fetch the next row from the given result object, in object form.
-        * Fields can be retrieved with $row->fieldname, with fields acting like
-        * member variables.
-        *
-        * @param $res SQL result object as returned from Database::query(), etc.
-        * @return MySQL row object
-        * @throws DBUnexpectedError Thrown if the database returns an error
-        */
-       function fetchObject( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @/**/$row = mysql_fetch_object( $res );
-               if( $this->lastErrno() ) {
-                       throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
-               }
-               return $row;
-       }
-
-       /**
-        * Fetch the next row from the given result object, in associative array
-        * form.  Fields are retrieved with $row['fieldname'].
-        *
-        * @param $res SQL result object as returned from Database::query(), etc.
-        * @return MySQL row object
-        * @throws DBUnexpectedError Thrown if the database returns an error
-        */
-       function fetchRow( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @/**/$row = mysql_fetch_array( $res );
-               if ( $this->lastErrno() ) {
-                       throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
-               }
-               return $row;
-       }
-
-       /**
-        * Get the number of rows in a result object
-        */
-       function numRows( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @/**/$n = mysql_num_rows( $res );
-               if( $this->lastErrno() ) {
-                       throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
-               }
-               return $n;
-       }
-
-       /**
-        * Get the number of fields in a result object
-        * See documentation for mysql_num_fields()
-        */
-       function numFields( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_num_fields( $res );
-       }
-
-       /**
-        * Get a field name in a result object
-        * See documentation for mysql_field_name():
-        * http://www.php.net/mysql_field_name
-        */
-       function fieldName( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_field_name( $res, $n );
-       }
-
-       /**
-        * Get the inserted value of an auto-increment row
-        *
-        * The value inserted should be fetched from nextSequenceValue()
-        *
-        * Example:
-        * $id = $dbw->nextSequenceValue('page_page_id_seq');
-        * $dbw->insert('page',array('page_id' => $id));
-        * $id = $dbw->insertId();
-        */
-       function insertId() { return mysql_insert_id( $this->mConn ); }
-
-       /**
-        * Change the position of the cursor in a result object
-        * See mysql_data_seek()
-        */
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_data_seek( $res, $row );
-       }
-
-       /**
-        * Get the last error number
-        * See mysql_errno()
-        */
-       function lastErrno() {
-               if ( $this->mConn ) {
-                       return mysql_errno( $this->mConn );
-               } else {
-                       return mysql_errno();
-               }
-       }
-
-       /**
-        * Get a description of the last error
-        * See mysql_error() for more details
-        */
-       function lastError() {
-               if ( $this->mConn ) {
-                       # Even if it's non-zero, it can still be invalid
-                       wfSuppressWarnings();
-                       $error = mysql_error( $this->mConn );
-                       if ( !$error ) {
-                               $error = mysql_error();
-                       }
-                       wfRestoreWarnings();
-               } else {
-                       $error = mysql_error();
-               }
-               if( $error ) {
-                       $error .= ' (' . $this->mServer . ')';
-               }
-               return $error;
-       }
-       /**
-        * Get the number of rows affected by the last write query
-        * See mysql_affected_rows() for more details
-        */
-       function affectedRows() { return mysql_affected_rows( $this->mConn ); }
-       /**#@-*/ // end of template : @param $result
-
-       /**
-        * Simple UPDATE wrapper
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns success
-        *
-        * This function exists for historical reasons, Database::update() has a more standard
-        * calling convention and feature set
-        */
-       function set( $table, $var, $value, $cond, $fname = 'Database::set' )
-       {
-               $table = $this->tableName( $table );
-               $sql = "UPDATE $table SET $var = '" .
-                 $this->strencode( $value ) . "' WHERE ($cond)";
-               return (bool)$this->query( $sql, $fname );
-       }
-
-       /**
-        * Simple SELECT wrapper, returns a single field, input must be encoded
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns FALSE on failure
-        */
-       function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
-               if ( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               $options['LIMIT'] = 1;
-
-               $res = $this->select( $table, $var, $cond, $fname, $options );
-               if ( $res === false || !$this->numRows( $res ) ) {
-                       return false;
-               }
-               $row = $this->fetchRow( $res );
-               if ( $row !== false ) {
-                       $this->freeResult( $res );
-                       return $row[0];
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Returns an optional USE INDEX clause to go after the table, and a
-        * string to go at the end of the query
-        *
-        * @private
-        *
-        * @param array $options an associative array of options to be turned into
-        *              an SQL query, valid keys are listed in the function.
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
-               $preLimitTail = $postLimitTail = '';
-               $startOpts = '';
-
-               $noKeyOptions = array();
-               foreach ( $options as $key => $option ) {
-                       if ( is_numeric( $key ) ) {
-                               $noKeyOptions[$option] = true;
-                       }
-               }
-
-               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
-               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
-               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-               
-               //if (isset($options['LIMIT'])) {
-               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
-               //              isset($options['OFFSET']) ? $options['OFFSET'] 
-               //              : false);
-               //}
-
-               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
-               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
-               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
-               # Various MySQL extensions
-               if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
-               if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
-               if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
-               if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
-               if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
-               if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
-               if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
-               if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
-
-               if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
-                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
-               } else {
-                       $useIndex = '';
-               }
-               
-               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
-       }
-
-       /**
-        * SELECT wrapper
-        *
-        * @param mixed  $table   Array or string, table name(s) (prefix auto-added)
-        * @param mixed  $vars    Array or string, field name(s) to be retrieved
-        * @param mixed  $conds   Array or string, condition(s) for WHERE
-        * @param string $fname   Calling function name (use __METHOD__) for logs/profiling
-        * @param array  $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
-        *                        see Database::makeSelectOptions code for list of supported stuff
-        * @param array $join_conds Associative array of table join conditions (optional)
-        *                        (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
-        * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
-        */
-       function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
-       {
-               $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
-               return $this->query( $sql, $fname );
-       }
-       
-       /**
-        * SELECT wrapper
-        *
-        * @param mixed  $table   Array or string, table name(s) (prefix auto-added)
-        * @param mixed  $vars    Array or string, field name(s) to be retrieved
-        * @param mixed  $conds   Array or string, condition(s) for WHERE
-        * @param string $fname   Calling function name (use __METHOD__) for logs/profiling
-        * @param array  $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
-        *                        see Database::makeSelectOptions code for list of supported stuff
-        * @param array $join_conds Associative array of table join conditions (optional)
-        *                        (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
-        * @return string, the SQL text
-        */
-       function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
-               if( is_array( $vars ) ) {
-                       $vars = implode( ',', $vars );
-               }
-               if( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               if( is_array( $table ) ) {
-                       if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
-                               $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
-                       else
-                               $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
-               } elseif ($table!='') {
-                       if ($table{0}==' ') {
-                               $from = ' FROM ' . $table;
-                       } else {
-                               $from = ' FROM ' . $this->tableName( $table );
-                       }
-               } else {
-                       $from = '';
-               }
-
-               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
-
-               if( !empty( $conds ) ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
-                       }
-                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
-               } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
-               }
-
-               if (isset($options['LIMIT']))
-                       $sql = $this->limitResult($sql, $options['LIMIT'],
-                               isset($options['OFFSET']) ? $options['OFFSET'] : false);
-               $sql = "$sql $postLimitTail";
-               
-               if (isset($options['EXPLAIN'])) {
-                       $sql = 'EXPLAIN ' . $sql;
-               }
-               return $sql;
-       }
-
-       /**
-        * Single row SELECT wrapper
-        * Aborts or returns FALSE on error
-        *
-        * $vars: the selected variables
-        * $conds: a condition map, terms are ANDed together.
-        *   Items with numeric keys are taken to be literal conditions
-        * Takes an array of selected variables, and a condition map, which is ANDed
-        * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
-        * NS_MAIN, "page_title" => "Astronomy" ) )   would return an object where
-        * $obj- >page_id is the ID of the Astronomy article
-        *
-        * @todo migrate documentation to phpdocumentor format
-        */
-       function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
-               $options['LIMIT'] = 1;
-               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
-               if ( $res === false )
-                       return false;
-               if ( !$this->numRows($res) ) {
-                       $this->freeResult($res);
-                       return false;
-               }
-               $obj = $this->fetchObject( $res );
-               $this->freeResult( $res );
-               return $obj;
-
-       }
-       
-       /**
-        * Estimate rows in dataset
-        * Returns estimated count, based on EXPLAIN output
-        * Takes same arguments as Database::select()
-        */
-       
-       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
-               $options['EXPLAIN']=true;
-               $res = $this->select ($table, $vars, $conds, $fname, $options );
-               if ( $res === false )
-                       return false;
-               if (!$this->numRows($res)) {
-                       $this->freeResult($res);
-                       return 0;
-               }
-               
-               $rows=1;
-       
-               while( $plan = $this->fetchObject( $res ) ) {
-                       $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
-               }
-               
-               $this->freeResult($res);
-               return $rows;           
-       }
-       
-
-       /**
-        * Removes most variables from an SQL query and replaces them with X or N for numbers.
-        * It's only slightly flawed. Don't use for anything important.
-        *
-        * @param string $sql A SQL Query
-        * @static
-        */
-       static function generalizeSQL( $sql ) {
-               # This does the same as the regexp below would do, but in such a way
-               # as to avoid crashing php on some large strings.
-               # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
-
-               $sql = str_replace ( "\\\\", '', $sql);
-               $sql = str_replace ( "\\'", '', $sql);
-               $sql = str_replace ( "\\\"", '', $sql);
-               $sql = preg_replace ("/'.*'/s", "'X'", $sql);
-               $sql = preg_replace ('/".*"/s', "'X'", $sql);
-
-               # All newlines, tabs, etc replaced by single space
-               $sql = preg_replace ( '/\s+/', ' ', $sql);
-
-               # All numbers => N
-               $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
-
-               return $sql;
-       }
-
-       /**
-        * Determines whether a field exists in a table
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns NULL on failure
-        */
-       function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
-               $table = $this->tableName( $table );
-               $res = $this->query( 'DESCRIBE '.$table, $fname );
-               if ( !$res ) {
-                       return NULL;
-               }
-
-               $found = false;
-
-               while ( $row = $this->fetchObject( $res ) ) {
-                       if ( $row->Field == $field ) {
-                               $found = true;
-                               break;
-                       }
-               }
-               return $found;
-       }
-
-       /**
-        * Determines whether an index exists
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns NULL on failure
-        */
-       function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
-               $info = $this->indexInfo( $table, $index, $fname );
-               if ( is_null( $info ) ) {
-                       return NULL;
-               } else {
-                       return $info !== false;
-               }
-       }
-
-
-       /**
-        * Get information about an index into an object
-        * Returns false if the index does not exist
-        */
-       function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
-               # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
-               # SHOW INDEX should work for 3.x and up:
-               # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
-               $table = $this->tableName( $table );
-               $sql = 'SHOW INDEX FROM '.$table;
-               $res = $this->query( $sql, $fname );
-               if ( !$res ) {
-                       return NULL;
-               }
-
-               $result = array();
-               while ( $row = $this->fetchObject( $res ) ) {
-                       if ( $row->Key_name == $index ) {
-                               $result[] = $row;
-                       }
-               }
-               $this->freeResult($res);
-               
-               return empty($result) ? false : $result;
-       }
-
-       /**
-        * Query whether a given table exists
-        */
-       function tableExists( $table ) {
-               $table = $this->tableName( $table );
-               $old = $this->ignoreErrors( true );
-               $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
-               $this->ignoreErrors( $old );
-               if( $res ) {
-                       $this->freeResult( $res );
-                       return true;
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * mysql_fetch_field() wrapper
-        * Returns false if the field doesn't exist
-        *
-        * @param $table
-        * @param $field
-        */
-       function fieldInfo( $table, $field ) {
-               $table = $this->tableName( $table );
-               $res = $this->query( "SELECT * FROM $table LIMIT 1" );
-               $n = mysql_num_fields( $res->result );
-               for( $i = 0; $i < $n; $i++ ) {
-                       $meta = mysql_fetch_field( $res->result, $i );
-                       if( $field == $meta->name ) {
-                               return new MySQLField($meta);
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * mysql_field_type() wrapper
-        */
-       function fieldType( $res, $index ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mysql_field_type( $res, $index );
-       }
-
-       /**
-        * Determines if a given index is unique
-        */
-       function indexUnique( $table, $index ) {
-               $indexInfo = $this->indexInfo( $table, $index );
-               if ( !$indexInfo ) {
-                       return NULL;
-               }
-               return !$indexInfo[0]->Non_unique;
-       }
-
-       /**
-        * INSERT wrapper, inserts an array into a table
-        *
-        * $a may be a single associative array, or an array of these with numeric keys, for
-        * multi-row insert.
-        *
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns success
-        */
-       function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
-               # No rows to insert, easy just return now
-               if ( !count( $a ) ) {
-                       return true;
-               }
-
-               $table = $this->tableName( $table );
-               if ( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
-                       $multi = true;
-                       $keys = array_keys( $a[0] );
-               } else {
-                       $multi = false;
-                       $keys = array_keys( $a );
-               }
-
-               $sql = 'INSERT ' . implode( ' ', $options ) .
-                       " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
-               if ( $multi ) {
-                       $first = true;
-                       foreach ( $a as $row ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $sql .= ',';
-                               }
-                               $sql .= '(' . $this->makeList( $row ) . ')';
-                       }
-               } else {
-                       $sql .= '(' . $this->makeList( $a ) . ')';
-               }
-               return (bool)$this->query( $sql, $fname );
-       }
-
-       /**
-        * Make UPDATE options for the Database::update function
-        *
-        * @private
-        * @param array $options The options passed to Database::update
-        * @return string
-        */
-       function makeUpdateOptions( $options ) {
-               if( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               $opts = array();
-               if ( in_array( 'LOW_PRIORITY', $options ) )
-                       $opts[] = $this->lowPriorityOption();
-               if ( in_array( 'IGNORE', $options ) )
-                       $opts[] = 'IGNORE';
-               return implode(' ', $opts);
-       }
-
-       /**
-        * UPDATE wrapper, takes a condition array and a SET array
-        *
-        * @param string $table  The table to UPDATE
-        * @param array  $values An array of values to SET
-        * @param array  $conds  An array of conditions (WHERE). Use '*' to update all rows.
-        * @param string $fname  The Class::Function calling this function
-        *                       (for the log)
-        * @param array  $options An array of UPDATE options, can be one or
-        *                        more of IGNORE, LOW_PRIORITY
-        * @return bool
-        */
-       function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
-               $table = $this->tableName( $table );
-               $opts = $this->makeUpdateOptions( $options );
-               $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
-               if ( $conds != '*' ) {
-                       $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
-               }
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Makes an encoded list of strings from an array
-        * $mode:
-        *        LIST_COMMA         - comma separated, no field names
-        *        LIST_AND           - ANDed WHERE clause (without the WHERE)
-        *        LIST_OR            - ORed WHERE clause (without the WHERE)
-        *        LIST_SET           - comma separated with field names, like a SET clause
-        *        LIST_NAMES         - comma separated field names
-        */
-       function makeList( $a, $mode = LIST_COMMA ) {
-               if ( !is_array( $a ) ) {
-                       throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
-               }
-
-               $first = true;
-               $list = '';
-               foreach ( $a as $field => $value ) {
-                       if ( !$first ) {
-                               if ( $mode == LIST_AND ) {
-                                       $list .= ' AND ';
-                               } elseif($mode == LIST_OR) {
-                                       $list .= ' OR ';
-                               } else {
-                                       $list .= ',';
-                               }
-                       } else {
-                               $first = false;
-                       }
-                       if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
-                               $list .= "($value)";
-                       } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
-                               $list .= "$value";
-                       } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
-                               if( count( $value ) == 0 ) {
-                                       throw new MWException( __METHOD__.': empty input' );
-                               } elseif( count( $value ) == 1 ) {
-                                       // Special-case single values, as IN isn't terribly efficient
-                                       // Don't necessarily assume the single key is 0; we don't
-                                       // enforce linear numeric ordering on other arrays here.
-                                       $value = array_values( $value );
-                                       $list .= $field." = ".$this->addQuotes( $value[0] );
-                               } else {
-                                       $list .= $field." IN (".$this->makeList($value).") ";
-                               }
-                       } elseif( is_null($value) ) {
-                               if ( $mode == LIST_AND || $mode == LIST_OR ) {
-                                       $list .= "$field IS ";
-                               } elseif ( $mode == LIST_SET ) {
-                                       $list .= "$field = ";
-                               }
-                               $list .= 'NULL';
-                       } else {
-                               if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
-                                       $list .= "$field = ";
-                               }
-                               $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
-                       }
-               }
-               return $list;
-       }
-
-       /**
-        * Change the current database
-        */
-       function selectDB( $db ) {
-               $this->mDBname = $db;
-               return mysql_select_db( $db, $this->mConn );
-       }
-
-       /**
-        * Get the current DB name
-        */
-       function getDBname() {
-               return $this->mDBname;
-       }
-
-       /**
-        * Get the server hostname or IP address
-        */
-       function getServer() {
-               return $this->mServer;
-       }
-
-       /**
-        * Format a table name ready for use in constructing an SQL query
-        *
-        * This does two important things: it quotes the table names to clean them up,
-        * and it adds a table prefix if only given a table name with no quotes.
-        *
-        * All functions of this object which require a table name call this function
-        * themselves. Pass the canonical name to such functions. This is only needed
-        * when calling query() directly.
-        *
-        * @param string $name database table name
-        * @return string full database name
-        */
-       function tableName( $name ) {
-               global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
-               # Skip the entire process when we have a string quoted on both ends.
-               # Note that we check the end so that we will still quote any use of
-               # use of `database`.table. But won't break things if someone wants
-               # to query a database table with a dot in the name.
-               if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
-               
-               # Lets test for any bits of text that should never show up in a table
-               # name. Basically anything like JOIN or ON which are actually part of
-               # SQL queries, but may end up inside of the table value to combine
-               # sql. Such as how the API is doing.
-               # Note that we use a whitespace test rather than a \b test to avoid
-               # any remote case where a word like on may be inside of a table name
-               # surrounded by symbols which may be considered word breaks.
-               if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
-               
-               # Split database and table into proper variables.
-               # We reverse the explode so that database.table and table both output
-               # the correct table.
-               $dbDetails = array_reverse( explode( '.', $name, 2 ) );
-               if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
-               else                         @list( $table ) = $dbDetails;
-               $prefix = $this->mTablePrefix; # Default prefix
-               
-               # A database name has been specified in input. Quote the table name
-               # because we don't want any prefixes added.
-               if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
-               
-               # Note that we use the long format because php will complain in in_array if
-               # the input is not an array, and will complain in is_array if it is not set.
-               if( !isset( $database ) # Don't use shared database if pre selected.
-                && isset( $wgSharedDB ) # We have a shared database
-                && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
-                && isset( $wgSharedTables )
-                && is_array( $wgSharedTables )
-                && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
-                       $database = $wgSharedDB;
-                       $prefix   = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
-               }
-               
-               # Quote the $database and $table and apply the prefix if not quoted.
-               if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
-               $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
-               
-               # Merge our database and table into our final table name.
-               $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
-               
-               # We're finished, return.
-               return $tableName;
-       }
-
-       /**
-        * Fetch a number of table names into an array
-        * This is handy when you need to construct SQL for joins
-        *
-        * Example:
-        * extract($dbr->tableNames('user','watchlist'));
-        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
-        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
-        */
-       public function tableNames() {
-               $inArray = func_get_args();
-               $retVal = array();
-               foreach ( $inArray as $name ) {
-                       $retVal[$name] = $this->tableName( $name );
-               }
-               return $retVal;
-       }
-       
-       /**
-        * Fetch a number of table names into an zero-indexed numerical array
-        * This is handy when you need to construct SQL for joins
-        *
-        * Example:
-        * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
-        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
-        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
-        */
-       public function tableNamesN() {
-               $inArray = func_get_args();
-               $retVal = array();
-               foreach ( $inArray as $name ) {
-                       $retVal[] = $this->tableName( $name );
-               }
-               return $retVal;
-       }
-
-       /**
-        * @private
-        */
-       function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
-               $ret = array();
-               $retJOIN = array();
-               $use_index_safe = is_array($use_index) ? $use_index : array();
-               $join_conds_safe = is_array($join_conds) ? $join_conds : array();
-               foreach ( $tables as $table ) {
-                       // Is there a JOIN and INDEX clause for this table?
-                       if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
-                               $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
-                               $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
-                               $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
-                               $retJOIN[] = $tableClause;
-                       // Is there an INDEX clause?
-                       } else if ( isset($use_index_safe[$table]) ) {
-                               $tableClause = $this->tableName( $table );
-                               $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
-                               $ret[] = $tableClause;
-                       // Is there a JOIN clause?
-                       } else if ( isset($join_conds_safe[$table]) ) {
-                               $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
-                               $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
-                               $retJOIN[] = $tableClause;
-                       } else {
-                               $tableClause = $this->tableName( $table );
-                               $ret[] = $tableClause;
-                       }
-               }
-               // We can't separate explicit JOIN clauses with ',', use ' ' for those
-               $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
-               $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
-               // Compile our final table clause
-               return implode(' ',array($straightJoins,$otherJoins) );
-       }
-
-       /**
-        * Wrapper for addslashes()
-        * @param string $s String to be slashed.
-        * @return string slashed string.
-        */
-       function strencode( $s ) {
-               return mysql_real_escape_string( $s, $this->mConn );
-       }
-
-       /**
-        * If it's a string, adds quotes and backslashes
-        * Otherwise returns as-is
-        */
-       function addQuotes( $s ) {
-               if ( is_null( $s ) ) {
-                       return 'NULL';
-               } else {
-                       # This will also quote numeric values. This should be harmless,
-                       # and protects against weird problems that occur when they really
-                       # _are_ strings such as article titles and string->number->string
-                       # conversion is not 1:1.
-                       return "'" . $this->strencode( $s ) . "'";
-               }
-       }
-
-       /**
-        * Escape string for safe LIKE usage
-        */
-       function escapeLike( $s ) {
-               $s=$this->strencode( $s );
-               $s=str_replace(array('%','_'),array('\%','\_'),$s);
-               return $s;
-       }
-
-       /**
-        * Returns an appropriately quoted sequence value for inserting a new row.
-        * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
-        * subclass will return an integer, and save the value for insertId()
-        */
-       function nextSequenceValue( $seqName ) {
-               return NULL;
-       }
-
-       /**
-        * USE INDEX clause
-        * PostgreSQL doesn't have them and returns ""
-        */
-       function useIndexClause( $index ) {
-               return "FORCE INDEX ($index)";
-       }
-
-       /**
-        * REPLACE query wrapper
-        * PostgreSQL simulates this with a DELETE followed by INSERT
-        * $row is the row to insert, an associative array
-        * $uniqueIndexes is an array of indexes. Each element may be either a
-        * field name or an array of field names
-        *
-        * It may be more efficient to leave off unique indexes which are unlikely to collide.
-        * However if you do this, you run the risk of encountering errors which wouldn't have
-        * occurred in MySQL
-        *
-        * @todo migrate comment to phodocumentor format
-        */
-       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
-               $table = $this->tableName( $table );
-
-               # Single row case
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = array( $rows );
-               }
-
-               $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
-               $first = true;
-               foreach ( $rows as $row ) {
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $sql .= ',';
-                       }
-                       $sql .= '(' . $this->makeList( $row ) . ')';
-               }
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * DELETE where the condition is a join
-        * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
-        *
-        * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
-        * join condition matches, set $conds='*'
-        *
-        * DO NOT put the join condition in $conds
-        *
-        * @param string $delTable The table to delete from.
-        * @param string $joinTable The other table.
-        * @param string $delVar The variable to join on, in the first table.
-        * @param string $joinVar The variable to join on, in the second table.
-        * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
-        */
-       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
-               }
-
-               $delTable = $this->tableName( $delTable );
-               $joinTable = $this->tableName( $joinTable );
-               $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
-               if ( $conds != '*' ) {
-                       $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
-               }
-
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Returns the size of a text field, or -1 for "unlimited"
-        */
-       function textFieldSize( $table, $field ) {
-               $table = $this->tableName( $table );
-               $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
-               $res = $this->query( $sql, 'Database::textFieldSize' );
-               $row = $this->fetchObject( $res );
-               $this->freeResult( $res );
-
-               $m = array();
-               if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
-                       $size = $m[1];
-               } else {
-                       $size = -1;
-               }
-               return $size;
-       }
-
-       /**
-        * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
-        */
-       function lowPriorityOption() {
-               return 'LOW_PRIORITY';
-       }
-
-       /**
-        * DELETE query wrapper
-        *
-        * Use $conds == "*" to delete all rows
-        */
-       function delete( $table, $conds, $fname = 'Database::delete' ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
-               }
-               $table = $this->tableName( $table );
-               $sql = "DELETE FROM $table";
-               if ( $conds != '*' ) {
-                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * INSERT SELECT wrapper
-        * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
-        * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
-        * $conds may be "*" to copy the whole table
-        * srcTable may be an array of tables.
-        */
-       function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
-               $insertOptions = array(), $selectOptions = array() )
-       {
-               $destTable = $this->tableName( $destTable );
-               if ( is_array( $insertOptions ) ) {
-                       $insertOptions = implode( ' ', $insertOptions );
-               }
-               if( !is_array( $selectOptions ) ) {
-                       $selectOptions = array( $selectOptions );
-               }
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
-               if( is_array( $srcTable ) ) {
-                       $srcTable =  implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
-               } else {
-                       $srcTable = $this->tableName( $srcTable );
-               }
-               $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
-                       " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex ";
-               if ( $conds != '*' ) {
-                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-               $sql .= " $tailOpts";
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Construct a LIMIT query with optional offset
-        * This is used for query pages
-        * $sql string SQL query we will append the limit too
-        * $limit integer the SQL limit
-        * $offset integer the SQL offset (default false)
-        */
-       function limitResult($sql, $limit, $offset=false) {
-               if( !is_numeric($limit) ) {
-                       throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
-               }
-               return "$sql LIMIT "
-                               . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
-                               . "{$limit} ";
-       }
-       function limitResultForUpdate($sql, $num) {
-               return $this->limitResult($sql, $num, 0);
-       }
-
-       /**
-        * Returns an SQL expression for a simple conditional.
-        * Uses IF on MySQL.
-        *
-        * @param string $cond SQL expression which will result in a boolean value
-        * @param string $trueVal SQL expression to return if true
-        * @param string $falseVal SQL expression to return if false
-        * @return string SQL fragment
-        */
-       function conditional( $cond, $trueVal, $falseVal ) {
-               return " IF($cond, $trueVal, $falseVal) ";
-       }
-
-       /**
-        * Returns a comand for str_replace function in SQL query.
-        * Uses REPLACE() in MySQL
-        *
-        * @param string $orig String or column to modify
-        * @param string $old String or column to seek
-        * @param string $new String or column to replace with
-        */
-       function strreplace( $orig, $old, $new ) {
-               return "REPLACE({$orig}, {$old}, {$new})";
-       }
-
-       /**
-        * Determines if the last failure was due to a deadlock
-        */
-       function wasDeadlock() {
-               return $this->lastErrno() == 1213;
-       }
-
-       /**
-        * Perform a deadlock-prone transaction.
-        *
-        * This function invokes a callback function to perform a set of write
-        * queries. If a deadlock occurs during the processing, the transaction
-        * will be rolled back and the callback function will be called again.
-        *
-        * Usage:
-        *   $dbw->deadlockLoop( callback, ... );
-        *
-        * Extra arguments are passed through to the specified callback function.
-        *
-        * Returns whatever the callback function returned on its successful,
-        * iteration, or false on error, for example if the retry limit was
-        * reached.
-        */
-       function deadlockLoop() {
-               $myFname = 'Database::deadlockLoop';
-
-               $this->begin();
-               $args = func_get_args();
-               $function = array_shift( $args );
-               $oldIgnore = $this->ignoreErrors( true );
-               $tries = DEADLOCK_TRIES;
-               if ( is_array( $function ) ) {
-                       $fname = $function[0];
-               } else {
-                       $fname = $function;
-               }
-               do {
-                       $retVal = call_user_func_array( $function, $args );
-                       $error = $this->lastError();
-                       $errno = $this->lastErrno();
-                       $sql = $this->lastQuery();
-
-                       if ( $errno ) {
-                               if ( $this->wasDeadlock() ) {
-                                       # Retry
-                                       usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
-                               } else {
-                                       $this->reportQueryError( $error, $errno, $sql, $fname );
-                               }
-                       }
-               } while( $this->wasDeadlock() && --$tries > 0 );
-               $this->ignoreErrors( $oldIgnore );
-               if ( $tries <= 0 ) {
-                       $this->query( 'ROLLBACK', $myFname );
-                       $this->reportQueryError( $error, $errno, $sql, $fname );
-                       return false;
-               } else {
-                       $this->query( 'COMMIT', $myFname );
-                       return $retVal;
-               }
-       }
-
-       /**
-        * Do a SELECT MASTER_POS_WAIT()
-        *
-        * @param string $file the binlog file
-        * @param string $pos the binlog position
-        * @param integer $timeout the maximum number of seconds to wait for synchronisation
-        */
-       function masterPosWait( MySQLMasterPos $pos, $timeout ) {
-               $fname = 'Database::masterPosWait';
-               wfProfileIn( $fname );
-
-               # Commit any open transactions
-               if ( $this->mTrxLevel ) {
-                       $this->immediateCommit();
-               }
-
-               if ( !is_null( $this->mFakeSlaveLag ) ) {
-                       $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
-                       if ( $wait > $timeout * 1e6 ) {
-                               wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
-                               wfProfileOut( $fname );
-                               return -1;
-                       } elseif ( $wait > 0 ) {
-                               wfDebug( "Fake slave waiting $wait us\n" );
-                               usleep( $wait );
-                               wfProfileOut( $fname );
-                               return 1;
-                       } else {
-                               wfDebug( "Fake slave up to date ($wait us)\n" );
-                               wfProfileOut( $fname );
-                               return 0;
-                       }
-               }
-
-               # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
-               $encFile = $this->addQuotes( $pos->file );
-               $encPos = intval( $pos->pos );
-               $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
-               $res = $this->doQuery( $sql );
-               if ( $res && $row = $this->fetchRow( $res ) ) {
-                       $this->freeResult( $res );
-                       wfProfileOut( $fname );
-                       return $row[0];
-               } else {
-                       wfProfileOut( $fname );
-                       return false;
-               }
-       }
-
-       /**
-        * Get the position of the master from SHOW SLAVE STATUS
-        */
-       function getSlavePos() {
-               if ( !is_null( $this->mFakeSlaveLag ) ) {
-                       $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
-                       wfDebug( __METHOD__.": fake slave pos = $pos\n" );
-                       return $pos;
-               }
-               $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
-               $row = $this->fetchObject( $res );
-               if ( $row ) {
-                       return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Get the position of the master from SHOW MASTER STATUS
-        */
-       function getMasterPos() {
-               if ( $this->mFakeMaster ) {
-                       return new MySQLMasterPos( 'fake', microtime( true ) );
-               }
-               $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
-               $row = $this->fetchObject( $res );
-               if ( $row ) {
-                       return new MySQLMasterPos( $row->File, $row->Position );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Begin a transaction, committing any previously open transaction
-        */
-       function begin( $fname = 'Database::begin' ) {
-               $this->query( 'BEGIN', $fname );
-               $this->mTrxLevel = 1;
-       }
-
-       /**
-        * End a transaction
-        */
-       function commit( $fname = 'Database::commit' ) {
-               $this->query( 'COMMIT', $fname );
-               $this->mTrxLevel = 0;
-       }
-
-       /**
-        * Rollback a transaction.
-        * No-op on non-transactional databases.
-        */
-       function rollback( $fname = 'Database::rollback' ) {
-               $this->query( 'ROLLBACK', $fname, true );
-               $this->mTrxLevel = 0;
-       }
-
-       /**
-        * Begin a transaction, committing any previously open transaction
-        * @deprecated use begin()
-        */
-       function immediateBegin( $fname = 'Database::immediateBegin' ) {
-               $this->begin();
-       }
-
-       /**
-        * Commit transaction, if one is open
-        * @deprecated use commit()
-        */
-       function immediateCommit( $fname = 'Database::immediateCommit' ) {
-               $this->commit();
-       }
-
-       /**
-        * Return MW-style timestamp used for MySQL schema
-        */
-       function timestamp( $ts=0 ) {
-               return wfTimestamp(TS_MW,$ts);
-       }
-
-       /**
-        * Local database timestamp format or null
-        */
-       function timestampOrNull( $ts = null ) {
-               if( is_null( $ts ) ) {
-                       return null;
-               } else {
-                       return $this->timestamp( $ts );
-               }
-       }
-
-       /**
-        * @todo document
-        */
-       function resultObject( $result ) {
-               if( empty( $result ) ) {
-                       return false;
-               } elseif ( $result instanceof ResultWrapper ) {
-                       return $result;
-               } elseif ( $result === true ) {
-                       // Successful write query
-                       return $result;
-               } else {
-                       return new ResultWrapper( $this, $result );
-               }
-       }
-
-       /**
-        * Return aggregated value alias
-        */
-       function aggregateValue ($valuedata,$valuename='value') {
-               return $valuename;
-       }
-
-       /**
-        * @return string wikitext of a link to the server software's web site
-        */
-       function getSoftwareLink() {
-               return "[http://www.mysql.com/ MySQL]";
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               return mysql_get_server_info( $this->mConn );
-       }
-
-       /**
-        * Ping the server and try to reconnect if it there is no connection
-        */
-       function ping() {
-               if( !function_exists( 'mysql_ping' ) ) {
-                       wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
-                       return true;
-               }
-               $ping = mysql_ping( $this->mConn );
-               if ( $ping ) {
-                       return true;
-               }
-
-               // Need to reconnect manually in MySQL client 5.0.13+
-               if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
-                       mysql_close( $this->mConn );
-                       $this->mOpened = false;
-                       $this->mConn = false;
-                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
-                       return true;
-               }
-               return false;
-       }
-
-       /**
-        * Get slave lag.
-        * At the moment, this will only work if the DB user has the PROCESS privilege
-        */
-       function getLag() {
-               if ( !is_null( $this->mFakeSlaveLag ) ) {
-                       wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
-                       return $this->mFakeSlaveLag;
-               }
-               $res = $this->query( 'SHOW PROCESSLIST' );
-               # Find slave SQL thread
-               while ( $row = $this->fetchObject( $res ) ) {
-                       /* This should work for most situations - when default db 
-                        * for thread is not specified, it had no events executed, 
-                        * and therefore it doesn't know yet how lagged it is.
-                        *
-                        * Relay log I/O thread does not select databases.
-                        */
-                       if ( $row->User == 'system user' && 
-                               $row->State != 'Waiting for master to send event' &&
-                               $row->State != 'Connecting to master' && 
-                               $row->State != 'Queueing master event to the relay log' &&
-                               $row->State != 'Waiting for master update' &&
-                               $row->State != 'Requesting binlog dump'
-                               ) {
-                               # This is it, return the time (except -ve)
-                               if ( $row->Time > 0x7fffffff ) {
-                                       return false;
-                               } else {
-                                       return $row->Time;
-                               }
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Get status information from SHOW STATUS in an associative array
-        */
-       function getStatus($which="%") {
-               $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
-               $status = array();
-               while ( $row = $this->fetchObject( $res ) ) {
-                       $status[$row->Variable_name] = $row->Value;
-               }
-               return $status;
-       }
-
-       /**
-        * Return the maximum number of items allowed in a list, or 0 for unlimited.
-        */
-       function maxListLen() {
-               return 0;
-       }
-
-       function encodeBlob($b) {
-               return $b;
-       }
-
-       function decodeBlob($b) {
-               return $b;
-       }
-
-       /**
-        * Override database's default connection timeout.
-        * May be useful for very long batch queries such as
-        * full-wiki dumps, where a single query reads out
-        * over hours or days.
-        * @param int $timeout in seconds
-        */
-       public function setTimeout( $timeout ) {
-               $this->query( "SET net_read_timeout=$timeout" );
-               $this->query( "SET net_write_timeout=$timeout" );
-       }
-
-       /**
-        * Read and execute SQL commands from a file.
-        * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
-        * @param string $filename File name to open
-        * @param callback $lineCallback Optional function called before reading each line
-        * @param callback $resultCallback Optional function called for each MySQL result
-        */
-       function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
-               $fp = fopen( $filename, 'r' );
-               if ( false === $fp ) {
-                       throw new MWException( "Could not open \"{$filename}\".\n" );
-               }
-               $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
-               fclose( $fp );
-               return $error;
-       }
-
-       /**
-        * Read and execute commands from an open file handle
-        * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
-        * @param string $fp File handle
-        * @param callback $lineCallback Optional function called before reading each line
-        * @param callback $resultCallback Optional function called for each MySQL result
-        */
-       function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
-               $cmd = "";
-               $done = false;
-               $dollarquote = false;
-
-               while ( ! feof( $fp ) ) {
-                       if ( $lineCallback ) {
-                               call_user_func( $lineCallback );
-                       }
-                       $line = trim( fgets( $fp, 1024 ) );
-                       $sl = strlen( $line ) - 1;
-
-                       if ( $sl < 0 ) { continue; }
-                       if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
-
-                       ## Allow dollar quoting for function declarations
-                       if (substr($line,0,4) == '$mw$') {
-                               if ($dollarquote) {
-                                       $dollarquote = false;
-                                       $done = true;
-                               }
-                               else {
-                                       $dollarquote = true;
-                               }
-                       }
-                       else if (!$dollarquote) {
-                               if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
-                                       $done = true;
-                                       $line = substr( $line, 0, $sl );
-                               }
-                       }
-
-                       if ( '' != $cmd ) { $cmd .= ' '; }
-                       $cmd .= "$line\n";
-
-                       if ( $done ) {
-                               $cmd = str_replace(';;', ";", $cmd);
-                               $cmd = $this->replaceVars( $cmd );
-                               $res = $this->query( $cmd, __METHOD__ );
-                               if ( $resultCallback ) {
-                                       call_user_func( $resultCallback, $res );
-                               }
-
-                               if ( false === $res ) {
-                                       $err = $this->lastError();
-                                       return "Query \"{$cmd}\" failed with error code \"$err\".\n";
-                               }
-
-                               $cmd = '';
-                               $done = false;
-                       }
-               }
-               return true;
-       }
-
-
-       /**
-        * Replace variables in sourced SQL
-        */
-       protected function replaceVars( $ins ) {
-               $varnames = array(
-                       'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
-                       'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
-                       'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
-               );
-
-               // Ordinary variables
-               foreach ( $varnames as $var ) {
-                       if( isset( $GLOBALS[$var] ) ) {
-                               $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
-                               $ins = str_replace( '{$' . $var . '}', $val, $ins );
-                               $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
-                               $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
-                       }
-               }
-
-               // Table prefixes
-               $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
-                       array( &$this, 'tableNameCallback' ), $ins );
-               return $ins;
-       }
-
-       /**
-        * Table name callback
-        * @private
-        */
-       protected function tableNameCallback( $matches ) {
-               return $this->tableName( $matches[1] );
-       }
-
-       /*
-        * Build a concatenation list to feed into a SQL query
-       */
-       function buildConcat( $stringList ) {
-               return 'CONCAT(' . implode( ',', $stringList ) . ')';
-       }
-       
-       /**
-        * Acquire a lock
-        * 
-        * Abstracted from Filestore::lock() so child classes can implement for
-        * their own needs.
-        * 
-        * @param string $lockName Name of lock to aquire
-        * @param string $method Name of method calling us
-        * @return bool
-        */
-       public function lock( $lockName, $method ) {
-               $lockName = $this->addQuotes( $lockName );
-               $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
-               $row = $this->fetchObject( $result );
-               $this->freeResult( $result );
-
-               if( $row->lockstatus == 1 ) {
-                       return true;
-               } else {
-                       wfDebug( __METHOD__." failed to acquire lock\n" );
-                       return false;
-               }
-       }
-       /**
-        * Release a lock.
-        * 
-        * @todo fixme - Figure out a way to return a bool
-        * based on successful lock release.
-        * 
-        * @param string $lockName Name of lock to release
-        * @param string $method Name of method calling us
-        */
-       public function unlock( $lockName, $method ) {
-               $lockName = $this->addQuotes( $lockName );
-               $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
-               $this->freeResult( $result );
-       }
-}
-
-/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends Database {
-       # Inherit all
-}
-
-/******************************************************************************
- * Utility classes
- *****************************************************************************/
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class DBObject {
-       public $mData;
-
-       function DBObject($data) {
-               $this->mData = $data;
-       }
-
-       function isLOB() {
-               return false;
-       }
-
-       function data() {
-               return $this->mData;
-       }
-}
-
-/**
- * Utility class
- * @ingroup Database
- *
- * This allows us to distinguish a blob from a normal string and an array of strings
- */
-class Blob {
-       private $mData;
-       function __construct($data) {
-               $this->mData = $data;
-       }
-       function fetch() {
-               return $this->mData;
-       }
-}
-
-/**
- * Utility class.
- * @ingroup Database
- */
-class MySQLField {
-       private $name, $tablename, $default, $max_length, $nullable,
-               $is_pk, $is_unique, $is_multiple, $is_key, $type;
-       function __construct ($info) {
-               $this->name = $info->name;
-               $this->tablename = $info->table;
-               $this->default = $info->def;
-               $this->max_length = $info->max_length;
-               $this->nullable = !$info->not_null;
-               $this->is_pk = $info->primary_key;
-               $this->is_unique = $info->unique_key;
-               $this->is_multiple = $info->multiple_key;
-               $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
-               $this->type = $info->type;
-       }
-
-       function name() {
-               return $this->name;
-       }
-
-       function tableName() {
-               return $this->tableName;
-       }
-
-       function defaultValue() {
-               return $this->default;
-       }
-
-       function maxLength() {
-               return $this->max_length;
-       }
-
-       function nullable() {
-               return $this->nullable;
-       }
-
-       function isKey() {
-               return $this->is_key;
-       }
-
-       function isMultipleKey() {
-               return $this->is_multiple;
-       }
-
-       function type() {
-               return $this->type;
-       }
-}
-
-/******************************************************************************
- * Error classes
- *****************************************************************************/
-
-/**
- * Database error base class
- * @ingroup Database
- */
-class DBError extends MWException {
-       public $db;
-
-       /**
-        * Construct a database error
-        * @param Database $db The database object which threw the error
-        * @param string $error A simple error message to be used for debugging
-        */
-       function __construct( Database &$db, $error ) {
-               $this->db =& $db;
-               parent::__construct( $error );
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DBConnectionError extends DBError {
-       public $error;
-       
-       function __construct( Database &$db, $error = 'unknown error' ) {
-               $msg = 'DB connection error';
-               if ( trim( $error ) != '' ) {
-                       $msg .= ": $error";
-               }
-               $this->error = $error;
-               parent::__construct( $db, $msg );
-       }
-
-       function useOutputPage() {
-               // Not likely to work
-               return false;
-       }
-
-       function useMessageCache() {
-               // Not likely to work
-               return false;
-       }
-       
-       function getText() {
-               return $this->getMessage() . "\n";
-       }
-
-       function getLogMessage() {
-               # Don't send to the exception log
-               return false;
-       }
-
-       function getPageTitle() {
-               global $wgSitename;
-               return "$wgSitename has a problem";
-       }
-
-       function getHTML() {
-               global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
-               global $wgSitename, $wgServer, $wgMessageCache;
-
-               # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
-               # Hard coding strings instead.
-
-               $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
-               $mainpage = 'Main Page';
-               $searchdisabled = <<<EOT
-<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
-<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
-EOT;
-
-               $googlesearch = "
-<!-- SiteSearch Google -->
-<FORM method=GET action=\"http://www.google.com/search\">
-<TABLE bgcolor=\"#FFFFFF\"><tr><td>
-<A HREF=\"http://www.google.com/\">
-<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
-border=\"0\" ALT=\"Google\"></A>
-</td>
-<td>
-<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
-<INPUT type=submit name=btnG VALUE=\"Google Search\">
-<font size=-1>
-<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
-<input type='hidden' name='ie' value='$2'>
-<input type='hidden' name='oe' value='$2'>
-</font>
-</td></tr></TABLE>
-</FORM>
-<!-- SiteSearch Google -->";
-               $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
-
-               # No database access
-               if ( is_object( $wgMessageCache ) ) {
-                       $wgMessageCache->disable();
-               }
-
-               if ( trim( $this->error ) == '' ) {
-                       $this->error = $this->db->getProperty('mServer');
-               }
-
-               $text = str_replace( '$1', $this->error, $noconnect );
-               $text .= wfGetSiteNotice();
-
-               if($wgUseFileCache) {
-                       if($wgTitle) {
-                               $t =& $wgTitle;
-                       } else {
-                               if($title) {
-                                       $t = Title::newFromURL( $title );
-                               } elseif (@/**/$_REQUEST['search']) {
-                                       $search = $_REQUEST['search'];
-                                       return $searchdisabled .
-                                         str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
-                                         $wgInputEncoding ), $googlesearch );
-                               } else {
-                                       $t = Title::newFromText( $mainpage );
-                               }
-                       }
-
-                       $cache = new HTMLFileCache( $t );
-                       if( $cache->isFileCached() ) {
-                               // @todo, FIXME: $msg is not defined on the next line.
-                               $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
-                                       $cachederror . "</b></p>\n";
-
-                               $tag = '<div id="article">';
-                               $text = str_replace(
-                                       $tag,
-                                       $tag . $msg,
-                                       $cache->fetchPageText() );
-                       }
-               }
-
-               return $text;
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DBQueryError extends DBError {
-       public $error, $errno, $sql, $fname;
-       
-       function __construct( Database &$db, $error, $errno, $sql, $fname ) {
-               $message = "A database error has occurred\n" .
-                 "Query: $sql\n" .
-                 "Function: $fname\n" .
-                 "Error: $errno $error\n";
-
-               parent::__construct( $db, $message );
-               $this->error = $error;
-               $this->errno = $errno;
-               $this->sql = $sql;
-               $this->fname = $fname;
-       }
-
-       function getText() {
-               if ( $this->useMessageCache() ) {
-                       return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
-                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
-               } else {
-                       return $this->getMessage();
-               }
-       }
-       
-       function getSQL() {
-               global $wgShowSQLErrors;
-               if( !$wgShowSQLErrors ) {
-                       return $this->msg( 'sqlhidden', 'SQL hidden' );
-               } else {
-                       return $this->sql;
-               }
-       }
-       
-       function getLogMessage() {
-               # Don't send to the exception log
-               return false;
-       }
-
-       function getPageTitle() {
-               return $this->msg( 'databaseerror', 'Database error' );
-       }
-
-       function getHTML() {
-               if ( $this->useMessageCache() ) {
-                       return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
-                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
-               } else {
-                       return nl2br( htmlspecialchars( $this->getMessage() ) );
-               }
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DBUnexpectedError extends DBError {}
-
-
-/**
- * Result wrapper for grabbing data queried by someone else
- * @ingroup Database
- */
-class ResultWrapper implements Iterator {
-       var $db, $result, $pos = 0, $currentRow = null;
-
-       /**
-        * Create a new result object from a result resource and a Database object
-        */
-       function ResultWrapper( $database, $result ) {
-               $this->db = $database;
-               if ( $result instanceof ResultWrapper ) {
-                       $this->result = $result->result;
-               } else {
-                       $this->result = $result;
-               }
-       }
-
-       /**
-        * Get the number of rows in a result object
-        */
-       function numRows() {
-               return $this->db->numRows( $this->result );
-       }
-
-       /**
-        * Fetch the next row from the given result object, in object form.
-        * Fields can be retrieved with $row->fieldname, with fields acting like
-        * member variables.
-        *
-        * @param $res SQL result object as returned from Database::query(), etc.
-        * @return MySQL row object
-        * @throws DBUnexpectedError Thrown if the database returns an error
-        */
-       function fetchObject() {
-               return $this->db->fetchObject( $this->result );
-       }
-
-       /**
-        * Fetch the next row from the given result object, in associative array
-        * form.  Fields are retrieved with $row['fieldname'].
-        *
-        * @param $res SQL result object as returned from Database::query(), etc.
-        * @return MySQL row object
-        * @throws DBUnexpectedError Thrown if the database returns an error
-        */
-       function fetchRow() {
-               return $this->db->fetchRow( $this->result );
-       }
-
-       /**
-        * Free a result object
-        */
-       function free() {
-               $this->db->freeResult( $this->result );
-               unset( $this->result );
-               unset( $this->db );
-       }
-
-       /**
-        * Change the position of the cursor in a result object
-        * See mysql_data_seek()
-        */
-       function seek( $row ) {
-               $this->db->dataSeek( $this->result, $row );
-       }
-
-       /*********************
-        * Iterator functions
-        * Note that using these in combination with the non-iterator functions
-        * above may cause rows to be skipped or repeated.
-        */
-
-       function rewind() {
-               if ($this->numRows()) {
-                       $this->db->dataSeek($this->result, 0);
-               }
-               $this->pos = 0;
-               $this->currentRow = null;
-       }
-
-       function current() {
-               if ( is_null( $this->currentRow ) ) {
-                       $this->next();
-               }
-               return $this->currentRow;
-       }
-
-       function key() {
-               return $this->pos;
-       }
-
-       function next() {
-               $this->pos++;
-               $this->currentRow = $this->fetchObject();
-               return $this->currentRow;
-       }
-
-       function valid() {
-               return $this->current() !== false;
-       }
-}
-
-class MySQLMasterPos {
-       var $file, $pos;
-
-       function __construct( $file, $pos ) {
-               $this->file = $file;
-               $this->pos = $pos;
-       }
-
-       function __toString() {
-               return "{$this->file}/{$this->pos}";
-       }
-}
diff --git a/includes/DatabaseMssql.php b/includes/DatabaseMssql.php
deleted file mode 100755 (executable)
index a6dda25..0000000
+++ /dev/null
@@ -1,1019 +0,0 @@
-<?php
-/**
- * This script is the MSSQL Server database abstraction layer
- *
- * See maintenance/mssql/README for development notes and other specific information
- * @ingroup Database
- * @file
- */
-
-/**
- * @ingroup Database
- */
-class DatabaseMssql extends Database {
-
-       var $mAffectedRows;
-       var $mLastResult;
-       var $mLastError;
-       var $mLastErrorNo;
-       var $mDatabaseFile;
-
-       /**
-        * Constructor
-        */
-       function __construct($server = false, $user = false, $password = false, $dbName = false,
-                       $failFunction = false, $flags = 0, $tablePrefix = 'get from global') {
-
-               global $wgOut, $wgDBprefix, $wgCommandLineMode;
-               if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet
-               $this->mOut =& $wgOut;
-               $this->mFailFunction = $failFunction;
-               $this->mFlags = $flags;
-
-               if ( $this->mFlags & DBO_DEFAULT ) {
-                       if ( $wgCommandLineMode ) {
-                               $this->mFlags &= ~DBO_TRX;
-                       } else {
-                               $this->mFlags |= DBO_TRX;
-                       }
-               }
-
-               /** Get the default table prefix*/
-               $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix;
-
-               if ($server) $this->open($server, $user, $password, $dbName);
-
-       }
-
-       /**
-        * todo: check if these should be true like parent class
-        */
-       function implicitGroupby()   { return false; }
-       function implicitOrderby()   { return false; }
-
-       static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
-               return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags);
-       }
-
-       /** Open an MSSQL database and return a resource handle to it
-        *  NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases
-        */
-       function open($server,$user,$password,$dbName) {
-               wfProfileIn(__METHOD__);
-
-               # Test for missing mysql.so
-               # First try to load it
-               if (!@extension_loaded('mssql')) {
-                       @dl('mssql.so');
-               }
-
-               # Fail now
-               # Otherwise we get a suppressed fatal error, which is very hard to track down
-               if (!function_exists( 'mssql_connect')) {
-                       throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" );
-               }
-
-               $this->close();
-               $this->mServer   = $server;
-               $this->mUser     = $user;
-               $this->mPassword = $password;
-               $this->mDBname   = $dbName;
-
-               wfProfileIn("dbconnect-$server");
-
-               # Try to connect up to three times
-               # The kernel's default SYN retransmission period is far too slow for us,
-               # so we use a short timeout plus a manual retry.
-               $this->mConn = false;
-               $max = 3;
-               for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
-                       if ( $i > 1 ) {
-                               usleep( 1000 );
-                       }
-                       if ($this->mFlags & DBO_PERSISTENT) {
-                               @/**/$this->mConn = mssql_pconnect($server, $user, $password);
-                       } else {
-                               # Create a new connection...
-                               @/**/$this->mConn = mssql_connect($server, $user, $password, true);
-                       }
-               }
-               
-               wfProfileOut("dbconnect-$server");
-
-               if ($dbName != '') {
-                       if ($this->mConn !== false) {
-                               $success = @/**/mssql_select_db($dbName, $this->mConn);
-                               if (!$success) {
-                                       $error = "Error selecting database $dbName on server {$this->mServer} " .
-                                               "from client host {$wguname['nodename']}\n";
-                                       wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
-                                       wfDebug( $error );
-                               }
-                       } else {
-                               wfDebug("DB connection error\n");
-                               wfDebug("Server: $server, User: $user, Password: ".substr($password, 0, 3)."...\n");
-                               $success = false;
-                       }
-               } else {
-                       # Delay USE query
-                       $success = (bool)$this->mConn;
-               }
-
-               if (!$success) $this->reportConnectionError();
-               $this->mOpened = $success;
-               wfProfileOut(__METHOD__);
-               return $success;
-       }
-
-       /**
-        * Close an MSSQL database
-        */
-       function close() {
-               $this->mOpened = false;
-               if ($this->mConn) {
-                       if ($this->trxLevel()) $this->immediateCommit();
-                       return mssql_close($this->mConn);
-               } else return true;
-       }
-
-       /**
-        * - MSSQL doesn't seem to do buffered results
-        * - the trasnaction syntax is modified here to avoid having to replicate
-        *   Database::query which uses BEGIN, COMMIT, ROLLBACK
-        */
-       function doQuery($sql) {
-               if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION';
-               $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing
-               $ret = mssql_query($sql, $this->mConn);
-               if ($ret === false) {
-                       $err = mssql_get_last_message();
-                       if ($err) $this->mlastError = $err;
-                       $row = mssql_fetch_row(mssql_query('select @@ERROR'));
-                       if ($row[0]) $this->mlastErrorNo = $row[0];
-               } else $this->mlastErrorNo = false;
-               return $ret;
-       }
-
-       /**#@+
-        * @param mixed $res A SQL result
-        */
-       /**
-        * Free a result object
-        */
-       function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               if ( !@/**/mssql_free_result( $res ) ) {
-                       throw new DBUnexpectedError( $this, "Unable to free MSSQL result" );
-               }
-       }
-
-       /**
-        * Fetch the next row from the given result object, in object form.
-        * Fields can be retrieved with $row->fieldname, with fields acting like
-        * member variables.
-        *
-        * @param $res SQL result object as returned from Database::query(), etc.
-        * @return MySQL row object
-        * @throws DBUnexpectedError Thrown if the database returns an error
-        */
-       function fetchObject( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @/**/$row = mssql_fetch_object( $res );
-               if ( $this->lastErrno() ) {
-                       throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
-               }
-               return $row;
-       }
-
-       /**
-        * Fetch the next row from the given result object, in associative array
-        * form.  Fields are retrieved with $row['fieldname'].
-        *
-        * @param $res SQL result object as returned from Database::query(), etc.
-        * @return MySQL row object
-        * @throws DBUnexpectedError Thrown if the database returns an error
-        */
-       function fetchRow( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @/**/$row = mssql_fetch_array( $res );
-               if ( $this->lastErrno() ) {
-                       throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
-               }
-               return $row;
-       }
-
-       /**
-        * Get the number of rows in a result object
-        */
-       function numRows( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @/**/$n = mssql_num_rows( $res );
-               if ( $this->lastErrno() ) {
-                       throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
-               }
-               return $n;
-       }
-
-       /**
-        * Get the number of fields in a result object
-        * See documentation for mysql_num_fields()
-        */
-       function numFields( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mssql_num_fields( $res );
-       }
-
-       /**
-        * Get a field name in a result object
-        * See documentation for mysql_field_name():
-        * http://www.php.net/mysql_field_name
-        */
-       function fieldName( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mssql_field_name( $res, $n );
-       }
-
-       /**
-        * Get the inserted value of an auto-increment row
-        *
-        * The value inserted should be fetched from nextSequenceValue()
-        *
-        * Example:
-        * $id = $dbw->nextSequenceValue('page_page_id_seq');
-        * $dbw->insert('page',array('page_id' => $id));
-        * $id = $dbw->insertId();
-        */
-       function insertId() {
-               $row = mssql_fetch_row(mssql_query('select @@IDENTITY'));
-               return $row[0];
-       }
-
-       /**
-        * Change the position of the cursor in a result object
-        * See mysql_data_seek()
-        */
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mssql_data_seek( $res, $row );
-       }
-
-       /**
-        * Get the last error number
-        */
-       function lastErrno() {
-               return $this->mlastErrorNo;
-       }
-
-       /**
-        * Get a description of the last error
-        */
-       function lastError() {
-               return $this->mlastError;
-       }
-
-       /**
-        * Get the number of rows affected by the last write query
-        */
-       function affectedRows() {
-               return mssql_rows_affected( $this->mConn );
-       }
-
-       /**
-        * Simple UPDATE wrapper
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns success
-        *
-        * This function exists for historical reasons, Database::update() has a more standard
-        * calling convention and feature set
-        */
-       function set( $table, $var, $value, $cond, $fname = 'Database::set' )
-       {
-               if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls()
-               $table = $this->tableName( $table );
-               $sql = "UPDATE $table SET $var = '" .
-                 $this->strencode( $value ) . "' WHERE ($cond)";
-               return (bool)$this->query( $sql, $fname );
-       }
-
-       /**
-        * Simple SELECT wrapper, returns a single field, input must be encoded
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns FALSE on failure
-        */
-       function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
-               if ( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               $options['LIMIT'] = 1;
-
-               $res = $this->select( $table, $var, $cond, $fname, $options );
-               if ( $res === false || !$this->numRows( $res ) ) {
-                       return false;
-               }
-               $row = $this->fetchRow( $res );
-               if ( $row !== false ) {
-                       $this->freeResult( $res );
-                       return $row[0];
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Returns an optional USE INDEX clause to go after the table, and a
-        * string to go at the end of the query
-        *
-        * @private
-        *
-        * @param array $options an associative array of options to be turned into
-        *              an SQL query, valid keys are listed in the function.
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
-               $preLimitTail = $postLimitTail = '';
-               $startOpts = '';
-
-               $noKeyOptions = array();
-               foreach ( $options as $key => $option ) {
-                       if ( is_numeric( $key ) ) {
-                               $noKeyOptions[$option] = true;
-                       }
-               }
-
-               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
-               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
-               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-               
-               //if (isset($options['LIMIT'])) {
-               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
-               //              isset($options['OFFSET']) ? $options['OFFSET'] 
-               //              : false);
-               //}
-
-               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
-               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
-               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
-               # Various MySQL extensions
-               if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
-               if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
-               if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
-               if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
-               if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
-               if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
-               if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
-               if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
-
-               if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
-                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
-               } else {
-                       $useIndex = '';
-               }
-               
-               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
-       }
-
-       /**
-        * SELECT wrapper
-        *
-        * @param mixed  $table   Array or string, table name(s) (prefix auto-added)
-        * @param mixed  $vars    Array or string, field name(s) to be retrieved
-        * @param mixed  $conds   Array or string, condition(s) for WHERE
-        * @param string $fname   Calling function name (use __METHOD__) for logs/profiling
-        * @param array  $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
-        *                        see Database::makeSelectOptions code for list of supported stuff
-        * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
-        */
-       function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
-       {
-               if( is_array( $vars ) ) {
-                       $vars = implode( ',', $vars );
-               }
-               if( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               if( is_array( $table ) ) {
-                       if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
-                               $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
-                       else
-                               $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
-               } elseif ($table!='') {
-                       if ($table{0}==' ') {
-                               $from = ' FROM ' . $table;
-                       } else {
-                               $from = ' FROM ' . $this->tableName( $table );
-                       }
-               } else {
-                       $from = '';
-               }
-
-               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
-
-               if( !empty( $conds ) ) {
-                       if ( is_array( $conds ) ) {
-                               $conds = $this->makeList( $conds, LIST_AND );
-                       }
-                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
-               } else {
-                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
-               }
-
-               if (isset($options['LIMIT']))
-                       $sql = $this->limitResult($sql, $options['LIMIT'],
-                               isset($options['OFFSET']) ? $options['OFFSET'] : false);
-               $sql = "$sql $postLimitTail";
-               
-               if (isset($options['EXPLAIN'])) {
-                       $sql = 'EXPLAIN ' . $sql;
-               }
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Estimate rows in dataset
-        * Returns estimated count, based on EXPLAIN output
-        * Takes same arguments as Database::select()
-        */
-       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
-               $rows = 0;
-               $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options );
-               if ($res) {
-                       $row = $this->fetchObject($res);
-                       $rows = $row[0];
-               }
-               $this->freeResult($res);
-               return $rows;
-       }
-       
-       /**
-        * Determines whether a field exists in a table
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns NULL on failure
-        */
-       function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
-               $table = $this->tableName( $table );
-               $sql = "SELECT TOP 1 * FROM $table";
-               $res = $this->query( $sql, 'Database::fieldExists' );
-
-               $found = false;
-               while ( $row = $this->fetchArray( $res ) ) {
-                       if ( isset($row[$field]) ) {
-                               $found = true;
-                               break;
-                       }
-               }
-
-               $this->freeResult( $res );
-               return $found;
-       }
-
-       /**
-        * Get information about an index into an object
-        * Returns false if the index does not exist
-        */
-       function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
-
-               throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
-               return NULL;
-
-               $table = $this->tableName( $table );
-               $sql = 'SHOW INDEX FROM '.$table;
-               $res = $this->query( $sql, $fname );
-               if ( !$res ) {
-                       return NULL;
-               }
-
-               $result = array();
-               while ( $row = $this->fetchObject( $res ) ) {
-                       if ( $row->Key_name == $index ) {
-                               $result[] = $row;
-                       }
-               }
-               $this->freeResult($res);
-               
-               return empty($result) ? false : $result;
-       }
-
-       /**
-        * Query whether a given table exists
-        */
-       function tableExists( $table ) {
-               $table = $this->tableName( $table );
-               $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" );
-               $exist = ($res->numRows() > 0);
-               $this->freeResult($res);
-               return $exist;
-       }
-
-       /**
-        * mysql_fetch_field() wrapper
-        * Returns false if the field doesn't exist
-        *
-        * @param $table
-        * @param $field
-        */
-       function fieldInfo( $table, $field ) {
-               $table = $this->tableName( $table );
-               $res = $this->query( "SELECT TOP 1 * FROM $table" );
-               $n = mssql_num_fields( $res->result );
-               for( $i = 0; $i < $n; $i++ ) {
-                       $meta = mssql_fetch_field( $res->result, $i );
-                       if( $field == $meta->name ) {
-                               return new MSSQLField($meta);
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * mysql_field_type() wrapper
-        */
-       function fieldType( $res, $index ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return mssql_field_type( $res, $index );
-       }
-
-       /**
-        * INSERT wrapper, inserts an array into a table
-        *
-        * $a may be a single associative array, or an array of these with numeric keys, for
-        * multi-row insert.
-        *
-        * Usually aborts on failure
-        * If errors are explicitly ignored, returns success
-        * 
-        * Same as parent class implementation except that it removes primary key from column lists
-        * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns
-        */
-       function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
-               # No rows to insert, easy just return now
-               if ( !count( $a ) ) {
-                       return true;
-               }
-               $table = $this->tableName( $table );
-               if ( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               
-               # todo: need to record primary keys at table create time, and remove NULL assignments to them
-               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
-                       $multi = true;
-                       $keys = array_keys( $a[0] );
-#                      if (ereg('_id$',$keys[0])) {
-                               foreach ($a as $i) {
-                                       if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value
-                               }
-#                      }
-                       $keys = array_keys( $a[0] );
-               } else {
-                       $multi = false;
-                       $keys = array_keys( $a );
-#                      if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
-                       if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
-                       $keys = array_keys( $a );
-               }
-
-               # handle IGNORE option
-               # example:
-               #   MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
-               #   MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
-               $ignore = in_array('IGNORE',$options);
-
-               # remove IGNORE from options list
-               if ($ignore) {
-                       $oldoptions = $options;
-                       $options = array();
-                       foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o;
-               }
-
-               $keylist = implode(',', $keys);
-               $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES ';
-               if ($multi) {
-                       if ($ignore) {
-                               # If multiple and ignore, then do each row as a separate conditional insert
-                               foreach ($a as $row) {
-                                       $prival = $row[$keys[0]];
-                                       $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
-                                       if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false;
-                               }
-                               return true;
-                       } else {
-                               $first = true;
-                               foreach ($a as $row) {
-                                       if ($first) $first = false; else $sql .= ',';
-                                       $sql .= '('.$this->makeListWithoutNulls($row).')';
-                               }
-                       }
-               } else {
-                       if ($ignore) {
-                               $prival = $a[$keys[0]];
-                               $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
-                       }
-                       $sql .= '('.$this->makeListWithoutNulls($a).')';
-               }
-               return (bool)$this->query( $sql, $fname );
-       }
-
-       /**
-        * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns
-        *   for now I've just converted the NULL's in the lists for updates and inserts into empty strings
-        *   which get implicitly casted to 0 for numeric columns
-        * NOTE: the set() method above converts NULL to empty string as well but not via this method
-        */
-       function makeListWithoutNulls($a, $mode = LIST_COMMA) {
-               return str_replace("NULL","''",$this->makeList($a,$mode));
-       }
-
-       /**
-        * UPDATE wrapper, takes a condition array and a SET array
-        *
-        * @param string $table  The table to UPDATE
-        * @param array  $values An array of values to SET
-        * @param array  $conds  An array of conditions (WHERE). Use '*' to update all rows.
-        * @param string $fname  The Class::Function calling this function
-        *                       (for the log)
-        * @param array  $options An array of UPDATE options, can be one or
-        *                        more of IGNORE, LOW_PRIORITY
-        * @return bool
-        */
-       function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
-               $table = $this->tableName( $table );
-               $opts = $this->makeUpdateOptions( $options );
-               $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET );
-               if ( $conds != '*' ) {
-                       $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
-               }
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Make UPDATE options for the Database::update function
-        *
-        * @private
-        * @param array $options The options passed to Database::update
-        * @return string
-        */
-       function makeUpdateOptions( $options ) {
-               if( !is_array( $options ) ) {
-                       $options = array( $options );
-               }
-               $opts = array();
-               if ( in_array( 'LOW_PRIORITY', $options ) )
-                       $opts[] = $this->lowPriorityOption();
-               if ( in_array( 'IGNORE', $options ) )
-                       $opts[] = 'IGNORE';
-               return implode(' ', $opts);
-       }
-
-       /**
-        * Change the current database
-        */
-       function selectDB( $db ) {
-               $this->mDBname = $db;
-               return mssql_select_db( $db, $this->mConn );
-       }
-
-       /**
-        * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once
-        */
-       function tableName($name) {
-               return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name";
-       }
-
-       /**
-        * MSSQL doubles quotes instead of escaping them
-        * @param string $s String to be slashed.
-        * @return string slashed string.
-        */
-       function strencode($s) {
-               return str_replace("'","''",$s);
-       }
-
-       /**
-        * USE INDEX clause
-        */
-       function useIndexClause( $index ) {
-               return "";
-       }
-
-       /**
-        * REPLACE query wrapper
-        * PostgreSQL simulates this with a DELETE followed by INSERT
-        * $row is the row to insert, an associative array
-        * $uniqueIndexes is an array of indexes. Each element may be either a
-        * field name or an array of field names
-        *
-        * It may be more efficient to leave off unique indexes which are unlikely to collide.
-        * However if you do this, you run the risk of encountering errors which wouldn't have
-        * occurred in MySQL
-        *
-        * @todo migrate comment to phodocumentor format
-        */
-       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
-               $table = $this->tableName( $table );
-
-               # Single row case
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = array( $rows );
-               }
-
-               $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
-               $first = true;
-               foreach ( $rows as $row ) {
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $sql .= ',';
-                       }
-                       $sql .= '(' . $this->makeList( $row ) . ')';
-               }
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * DELETE where the condition is a join
-        * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
-        *
-        * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
-        * join condition matches, set $conds='*'
-        *
-        * DO NOT put the join condition in $conds
-        *
-        * @param string $delTable The table to delete from.
-        * @param string $joinTable The other table.
-        * @param string $delVar The variable to join on, in the first table.
-        * @param string $joinVar The variable to join on, in the second table.
-        * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
-        */
-       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
-               }
-
-               $delTable = $this->tableName( $delTable );
-               $joinTable = $this->tableName( $joinTable );
-               $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
-               if ( $conds != '*' ) {
-                       $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
-               }
-
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Returns the size of a text field, or -1 for "unlimited"
-        */
-       function textFieldSize( $table, $field ) {
-               $table = $this->tableName( $table );
-               $sql = "SELECT TOP 1 * FROM $table;";
-               $res = $this->query( $sql, 'Database::textFieldSize' );
-               $row = $this->fetchObject( $res );
-               $this->freeResult( $res );
-
-               $m = array();
-               if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
-                       $size = $m[1];
-               } else {
-                       $size = -1;
-               }
-               return $size;
-       }
-
-       /**
-        * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
-        */
-       function lowPriorityOption() {
-               return 'LOW_PRIORITY';
-       }
-
-       /**
-        * INSERT SELECT wrapper
-        * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
-        * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
-        * $conds may be "*" to copy the whole table
-        * srcTable may be an array of tables.
-        */
-       function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
-               $insertOptions = array(), $selectOptions = array() )
-       {
-               $destTable = $this->tableName( $destTable );
-               if ( is_array( $insertOptions ) ) {
-                       $insertOptions = implode( ' ', $insertOptions );
-               }
-               if( !is_array( $selectOptions ) ) {
-                       $selectOptions = array( $selectOptions );
-               }
-               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
-               if( is_array( $srcTable ) ) {
-                       $srcTable =  implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
-               } else {
-                       $srcTable = $this->tableName( $srcTable );
-               }
-               $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
-                       " SELECT $startOpts " . implode( ',', $varMap ) .
-                       " FROM $srcTable $useIndex ";
-               if ( $conds != '*' ) {
-                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-               $sql .= " $tailOpts";
-               return $this->query( $sql, $fname );
-       }
-
-       /**
-        * Construct a LIMIT query with optional offset
-        * This is used for query pages
-        * $sql string SQL query we will append the limit to
-        * $limit integer the SQL limit
-        * $offset integer the SQL offset (default false)
-        */
-       function limitResult($sql, $limit, $offset=false) {
-               if( !is_numeric($limit) ) {
-                       throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
-               }
-               if ($offset) {
-                       throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' );
-               } else {
-                       $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql);
-               }
-               return $sql;
-       }
-
-       /**
-        * Returns an SQL expression for a simple conditional.
-        *
-        * @param string $cond SQL expression which will result in a boolean value
-        * @param string $trueVal SQL expression to return if true
-        * @param string $falseVal SQL expression to return if false
-        * @return string SQL fragment
-        */
-       function conditional( $cond, $trueVal, $falseVal ) {
-               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
-       }
-
-       /**
-        * Should determine if the last failure was due to a deadlock
-        * - don't know how to do this in MSSQL
-        */
-       function wasDeadlock() {
-               return false;
-       }
-
-       /**
-        * Begin a transaction, committing any previously open transaction
-        * @deprecated use begin()
-        */
-       function immediateBegin( $fname = 'Database::immediateBegin' ) {
-               $this->begin();
-       }
-
-       /**
-        * Commit transaction, if one is open
-        * @deprecated use commit()
-        */
-       function immediateCommit( $fname = 'Database::immediateCommit' ) {
-               $this->commit();
-       }
-
-       /**
-        * Return MW-style timestamp used for MySQL schema
-        */
-       function timestamp( $ts=0 ) {
-               return wfTimestamp(TS_MW,$ts);
-       }
-
-       /**
-        * Local database timestamp format or null
-        */
-       function timestampOrNull( $ts = null ) {
-               if( is_null( $ts ) ) {
-                       return null;
-               } else {
-                       return $this->timestamp( $ts );
-               }
-       }
-
-       /**
-        * @return string wikitext of a link to the server software's web site
-        */
-       function getSoftwareLink() {
-               return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]";
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               $row = mssql_fetch_row(mssql_query('select @@VERSION'));
-               return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0];
-       }
-
-       function limitResultForUpdate($sql, $num) {
-               return $sql;
-       }
-
-       /**
-        * not done
-        */
-       public function setTimeout($timeout) { return; }
-
-       function ping() {
-               wfDebug("Function ping() not written for MSSQL yet");
-               return true;
-       }
-
-       /**
-        * How lagged is this slave?
-        */
-       public function getLag() {
-               return 0;
-       }
-
-       /**
-        * Called by the installer script
-        * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
-        */
-       public function setup_database() {
-               global $IP,$wgDBTableOptions;
-               $wgDBTableOptions = '';
-               $mysql_tmpl = "$IP/maintenance/tables.sql";
-               $mysql_iw   = "$IP/maintenance/interwiki.sql";
-               $mssql_tmpl = "$IP/maintenance/mssql/tables.sql";
-
-               # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
-               if (!file_exists($mssql_tmpl)) { # todo: make this conditional again
-                       $sql = file_get_contents($mysql_tmpl);
-                       $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
-                       $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query
-                       $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)"
-                       $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary"
-                       $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits)
-                       $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary
-                       $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar
-                       #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead
-                       #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string
-                       $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL
-                       $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options
-                       $sql = preg_replace('/ (un)?signed/i', '', $sql);
-                       $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
-                       $sql = str_replace(' bool ', ' bit ', $sql);
-                       $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql);
-                       #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns
-
-                       # Tidy up and write file
-                       $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals
-                       $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines
-                       $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line
-                       file_put_contents($mssql_tmpl, $sql);
-               }
-
-               # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/
-               $err = $this->sourceFile($mssql_tmpl);
-               if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
-
-               # Use DatabasePostgres's code to populate interwiki from MySQL template
-               $f = fopen($mysql_iw,'r');
-               if ($f == false) dieout("<li>Could not find the interwiki.sql file");
-               $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES ";
-               while (!feof($f)) {
-                       $line = fgets($f,1024);
-                       $matches = array();
-                       if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
-                       $this->query("$sql $matches[1],$matches[2])");
-               }
-       }
-
-}
-
-/**
- * @ingroup Database
- */
-class MSSQLField extends MySQLField {
-
-       function __construct() {
-       }
-
-       static function fromText($db, $table, $field) {
-               $n = new MSSQLField;
-               $n->name = $field;
-               $n->tablename = $table;
-               return $n;
-       }
-
-} // end DatabaseMssql class
-
diff --git a/includes/DatabaseOracle.php b/includes/DatabaseOracle.php
deleted file mode 100644 (file)
index 8d2a675..0000000
+++ /dev/null
@@ -1,710 +0,0 @@
-<?php
-/**
- * @ingroup Database
- * @file
- */
-
-/**
- * This is the Oracle database abstraction layer.
- * @ingroup Database
- */
-class ORABlob {
-       var $mData;
-
-       function __construct($data) {
-               $this->mData = $data;
-       }
-
-       function getData() {
-               return $this->mData;
-       }
-}
-
-/**
- * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
- * other things.  We use a wrapper class to handle that and other
- * Oracle-specific bits, like converting column names back to lowercase.
- * @ingroup Database
- */
-class ORAResult {
-       private $rows;
-       private $cursor;
-       private $stmt;
-       private $nrows;
-       private $db;
-
-       function __construct(&$db, $stmt) {
-               $this->db =& $db;
-               if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) {
-                       $e = oci_error($stmt);
-                       $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__);
-                       return;
-               }
-
-               $this->cursor = 0;
-               $this->stmt = $stmt;
-       }
-
-       function free() {
-               oci_free_statement($this->stmt);
-       }
-
-       function seek($row) {
-               $this->cursor = min($row, $this->nrows);
-       }
-
-       function numRows() {
-               return $this->nrows;
-       }
-
-       function numFields() {
-               return oci_num_fields($this->stmt);
-       }
-
-       function fetchObject() {
-               if ($this->cursor >= $this->nrows)
-                       return false;
-
-               $row = $this->rows[$this->cursor++];
-               $ret = new stdClass();
-               foreach ($row as $k => $v) {
-                       $lc = strtolower(oci_field_name($this->stmt, $k + 1));
-                       $ret->$lc = $v;
-               }
-
-               return $ret;
-       }
-
-       function fetchAssoc() {
-               if ($this->cursor >= $this->nrows)
-                       return false;
-
-               $row = $this->rows[$this->cursor++];
-               $ret = array();
-               foreach ($row as $k => $v) {
-                       $lc = strtolower(oci_field_name($this->stmt, $k + 1));
-                       $ret[$lc] = $v;
-                       $ret[$k] = $v;
-               }
-               return $ret;
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DatabaseOracle extends Database {
-       var $mInsertId = NULL;
-       var $mLastResult = NULL;
-       var $numeric_version = NULL;
-       var $lastResult = null;
-       var $cursor = 0;
-       var $mAffectedRows;
-
-       function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false,
-               $failFunction = false, $flags = 0 )
-       {
-
-               global $wgOut;
-               # Can't get a reference if it hasn't been set yet
-               if ( !isset( $wgOut ) ) {
-                       $wgOut = NULL;
-               }
-               $this->mOut =& $wgOut;
-               $this->mFailFunction = $failFunction;
-               $this->mFlags = $flags;
-               $this->open( $server, $user, $password, $dbName);
-
-       }
-
-       function cascadingDeletes() {
-               return true;
-       }
-       function cleanupTriggers() {
-               return true;
-       }
-       function strictIPs() {
-               return true;
-       }
-       function realTimestamps() {
-               return true;
-       }
-       function implicitGroupby() {
-               return false;
-       }
-       function implicitOrderby() {
-               return false;
-       }
-       function searchableIPs() {
-               return true;
-       }
-
-       static function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
-               $failFunction = false, $flags = 0)
-       {
-               return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
-       }
-
-       /**
-        * Usually aborts on failure
-        * If the failFunction is set to a non-zero integer, returns success
-        */
-       function open( $server, $user, $password, $dbName ) {
-               if ( !function_exists( 'oci_connect' ) ) {
-                       throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
-               }
-
-               # Needed for proper UTF-8 functionality
-               putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8");
-
-               $this->close();
-               $this->mServer = $server;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
-
-               if (!strlen($user)) { ## e.g. the class is being loaded
-                       return;
-               }
-
-               error_reporting( E_ALL );
-               $this->mConn = oci_connect($user, $password, $dbName);
-
-               if ($this->mConn == false) {
-                       wfDebug("DB connection error\n");
-                       wfDebug("Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n");
-                       wfDebug($this->lastError()."\n");
-                       return false;
-               }
-
-               $this->mOpened = true;
-               return $this->mConn;
-       }
-
-       /**
-        * Closes a database connection, if it is open
-        * Returns success, true if already closed
-        */
-       function close() {
-               $this->mOpened = false;
-               if ( $this->mConn ) {
-                       return oci_close( $this->mConn );
-               } else {
-                       return true;
-               }
-       }
-
-       function execFlags() {
-               return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS;
-       }
-
-       function doQuery($sql) {
-               wfDebug("SQL: [$sql]\n");
-               if (!mb_check_encoding($sql)) {
-                       throw new MWException("SQL encoding is invalid");
-               }
-
-               if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) {
-                       $e = oci_error($this->mConn);
-                       $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
-               }
-
-               if (oci_execute($stmt, $this->execFlags()) == false) {
-                       $e = oci_error($stmt);
-                       $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
-               }
-               if (oci_statement_type($stmt) == "SELECT")
-                       return new ORAResult($this, $stmt);
-               else {
-                       $this->mAffectedRows = oci_num_rows($stmt);
-                       return true;
-               }
-       }
-
-       function queryIgnore($sql, $fname = '') {
-               return $this->query($sql, $fname, true);
-       }
-
-       function freeResult($res) {
-               $res->free();
-       }
-
-       function fetchObject($res) {
-               return $res->fetchObject();
-       }
-
-       function fetchRow($res) {
-               return $res->fetchAssoc();
-       }
-
-       function numRows($res) {
-               return $res->numRows();
-       }
-
-       function numFields($res) {
-               return $res->numFields();
-       }
-
-       function fieldName($stmt, $n) {
-               return pg_field_name($stmt, $n);
-       }
-
-       /**
-        * This must be called after nextSequenceVal
-        */
-       function insertId() {
-               return $this->mInsertId;
-       }
-
-       function dataSeek($res, $row) {
-               $res->seek($row);
-       }
-
-       function lastError() {
-               if ($this->mConn === false)
-                       $e = oci_error();
-               else
-                       $e = oci_error($this->mConn);
-               return $e['message'];
-       }
-
-       function lastErrno() {
-               if ($this->mConn === false)
-                       $e = oci_error();
-               else
-                       $e = oci_error($this->mConn);
-               return $e['code'];
-       }
-
-       function affectedRows() {
-               return $this->mAffectedRows;
-       }
-
-       /**
-        * Returns information about an index
-        * If errors are explicitly ignored, returns NULL on failure
-        */
-       function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
-               return false;
-       }
-
-       function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
-               return false;
-       }
-
-       function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
-               if (!is_array($options))
-                       $options = array($options);
-
-               #if (in_array('IGNORE', $options))
-               #       $oldIgnore = $this->ignoreErrors(true);
-
-               # IGNORE is performed using single-row inserts, ignoring errors in each
-               # FIXME: need some way to distiguish between key collision and other types of error
-               //$oldIgnore = $this->ignoreErrors(true);
-               if (!is_array(reset($a))) {
-                       $a = array($a);
-               }
-               foreach ($a as $row) {
-                       $this->insertOneRow($table, $row, $fname);
-               }
-               //$this->ignoreErrors($oldIgnore);
-               $retVal = true;
-
-               //if (in_array('IGNORE', $options))
-               //      $this->ignoreErrors($oldIgnore);
-
-               return $retVal;
-       }
-
-       function insertOneRow($table, $row, $fname) {
-               // "INSERT INTO tables (a, b, c)"
-               $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')';
-               $sql .= " VALUES (";
-
-               // for each value, append ":key"
-               $first = true;
-               $returning = '';
-               foreach ($row as $col => $val) {
-                       if (is_object($val)) {
-                               $what = "EMPTY_BLOB()";
-                               assert($returning === '');
-                               $returning = " RETURNING $col INTO :bval";
-                               $blobcol = $col;
-                       } else
-                               $what = ":$col";
-
-                       if ($first)
-                               $sql .= "$what";
-                       else
-                               $sql.= ", $what";
-                       $first = false;
-               }
-               $sql .= ") $returning";
-
-               $stmt = oci_parse($this->mConn, $sql);
-               foreach ($row as $col => $val) {
-                       if (!is_object($val)) {
-                               if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false)
-                                       $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__);
-                       }
-               }
-
-               if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) {
-                       $e = oci_error($stmt);
-                       throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']);
-               }
-
-               if (strlen($returning))
-                       oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB);
-
-               if (oci_execute($stmt, OCI_DEFAULT) === false) {
-                       $e = oci_error($stmt);
-                       $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__);
-               }
-               if (strlen($returning)) {
-                       $bval->save($row[$blobcol]->getData());
-                       $bval->free();
-               }
-               if (!$this->mTrxLevel)
-                       oci_commit($this->mConn);
-
-               oci_free_statement($stmt);
-       }
-
-       function tableName( $name ) {
-               # Replace reserved words with better ones
-               switch( $name ) {
-                       case 'user':
-                               return 'mwuser';
-                       case 'text':
-                               return 'pagecontent';
-                       default:
-                               return $name;
-               }
-       }
-
-       /**
-        * Return the next in a sequence, save the value for retrieval via insertId()
-        */
-       function nextSequenceValue($seqName) {
-               $res = $this->query("SELECT $seqName.nextval FROM dual");
-               $row = $this->fetchRow($res);
-               $this->mInsertId = $row[0];
-               $this->freeResult($res);
-               return $this->mInsertId;
-       }
-
-       /**
-        * Oracle does not have a "USE INDEX" clause, so return an empty string
-        */
-       function useIndexClause($index) {
-               return '';
-       }
-
-       # REPLACE query wrapper
-       # Oracle simulates this with a DELETE followed by INSERT
-       # $row is the row to insert, an associative array
-       # $uniqueIndexes is an array of indexes. Each element may be either a
-       # field name or an array of field names
-       #
-       # It may be more efficient to leave off unique indexes which are unlikely to collide.
-       # However if you do this, you run the risk of encountering errors which wouldn't have
-       # occurred in MySQL
-       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
-               $table = $this->tableName($table);
-
-               if (count($rows)==0) {
-                       return;
-               }
-
-               # Single row case
-               if (!is_array(reset($rows))) {
-                       $rows = array($rows);
-               }
-
-               foreach( $rows as $row ) {
-                       # Delete rows which collide
-                       if ( $uniqueIndexes ) {
-                               $sql = "DELETE FROM $table WHERE ";
-                               $first = true;
-                               foreach ( $uniqueIndexes as $index ) {
-                                       if ( $first ) {
-                                               $first = false;
-                                               $sql .= "(";
-                                       } else {
-                                               $sql .= ') OR (';
-                                       }
-                                       if ( is_array( $index ) ) {
-                                               $first2 = true;
-                                               foreach ( $index as $col ) {
-                                                       if ( $first2 ) {
-                                                               $first2 = false;
-                                                       } else {
-                                                               $sql .= ' AND ';
-                                                       }
-                                                       $sql .= $col.'=' . $this->addQuotes( $row[$col] );
-                                               }
-                                       } else {
-                                               $sql .= $index.'=' . $this->addQuotes( $row[$index] );
-                                       }
-                               }
-                               $sql .= ')';
-                               $this->query( $sql, $fname );
-                       }
-
-                       # Now insert the row
-                       $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
-                               $this->makeList( $row, LIST_COMMA ) . ')';
-                       $this->query($sql, $fname);
-               }
-       }
-
-       # DELETE where the condition is a join
-       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError($this,  'Database::deleteJoin() called with empty $conds' );
-               }
-
-               $delTable = $this->tableName( $delTable );
-               $joinTable = $this->tableName( $joinTable );
-               $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
-               if ( $conds != '*' ) {
-                       $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-               $sql .= ')';
-
-               $this->query( $sql, $fname );
-       }
-
-       # Returns the size of a text field, or -1 for "unlimited"
-       function textFieldSize( $table, $field ) {
-               $table = $this->tableName( $table );
-               $sql = "SELECT t.typname as ftype,a.atttypmod as size
-                       FROM pg_class c, pg_attribute a, pg_type t
-                       WHERE relname='$table' AND a.attrelid=c.oid AND
-                               a.atttypid=t.oid and a.attname='$field'";
-               $res =$this->query($sql);
-               $row=$this->fetchObject($res);
-               if ($row->ftype=="varchar") {
-                       $size=$row->size-4;
-               } else {
-                       $size=$row->size;
-               }
-               $this->freeResult( $res );
-               return $size;
-       }
-
-       function lowPriorityOption() {
-               return '';
-       }
-
-       function limitResult($sql, $limit, $offset) {
-               if ($offset === false)
-                       $offset = 0;
-               return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset";
-       }
-
-       /**
-        * Returns an SQL expression for a simple conditional.
-        * Uses CASE on Oracle
-        *
-        * @param string $cond SQL expression which will result in a boolean value
-        * @param string $trueVal SQL expression to return if true
-        * @param string $falseVal SQL expression to return if false
-        * @return string SQL fragment
-        */
-       function conditional( $cond, $trueVal, $falseVal ) {
-               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
-       }
-
-       function wasDeadlock() {
-               return $this->lastErrno() == 'OCI-00060';
-       }
-
-       function timestamp($ts = 0) {
-               return wfTimestamp(TS_ORACLE, $ts);
-       }
-
-       /**
-        * Return aggregated value function call
-        */
-       function aggregateValue ($valuedata,$valuename='value') {
-               return $valuedata;
-       }
-
-       function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) {
-               # Ignore errors during error handling to avoid infinite
-               # recursion
-               $ignore = $this->ignoreErrors(true);
-               ++$this->mErrorCount;
-
-               if ($ignore || $tempIgnore) {
-echo "error ignored! query = [$sql]\n";
-                       wfDebug("SQL ERROR (ignored): $error\n");
-                       $this->ignoreErrors( $ignore );
-               }
-               else {
-echo "error!\n";
-                       $message = "A database error has occurred\n" .
-                               "Query: $sql\n" .
-                               "Function: $fname\n" .
-                               "Error: $errno $error\n";
-                       throw new DBUnexpectedError($this, $message);
-               }
-       }
-
-       /**
-        * @return string wikitext of a link to the server software's web site
-        */
-       function getSoftwareLink() {
-               return "[http://www.oracle.com/ Oracle]";
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               return oci_server_version($this->mConn);
-       }
-
-       /**
-        * Query whether a given table exists (in the given schema, or the default mw one if not given)
-        */
-       function tableExists($table) {
-               $etable= $this->addQuotes($table);
-               $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'";
-               $res = $this->query($SQL);
-               $count = $res ? oci_num_rows($res) : 0;
-               if ($res)
-                       $this->freeResult($res);
-               return $count;
-       }
-
-       /**
-        * Query whether a given column exists in the mediawiki schema
-        */
-       function fieldExists( $table, $field ) {
-               return true; // XXX
-       }
-
-       function fieldInfo( $table, $field ) {
-               return false; // XXX
-       }
-
-       function begin( $fname = '' ) {
-               $this->mTrxLevel = 1;
-       }
-       function immediateCommit( $fname = '' ) {
-               return true;
-       }
-       function commit( $fname = '' ) {
-               oci_commit($this->mConn);
-               $this->mTrxLevel = 0;
-       }
-
-       /* Not even sure why this is used in the main codebase... */
-       function limitResultForUpdate($sql, $num) {
-               return $sql;
-       }
-
-       function strencode($s) {
-               return str_replace("'", "''", $s);
-       }
-
-       function encodeBlob($b) {
-               return new ORABlob($b);
-       }
-       function decodeBlob($b) {
-               return $b; //return $b->load();
-       }
-
-       function addQuotes( $s ) {
-       global  $wgLang;
-               $s = $wgLang->checkTitleEncoding($s);
-               return "'" . $this->strencode($s) . "'";
-       }
-
-       function quote_ident( $s ) {
-               return $s;
-       }
-
-       /* For now, does nothing */
-       function selectDB( $db ) {
-               return true;
-       }
-
-       /**
-        * Returns an optional USE INDEX clause to go after the table, and a
-        * string to go at the end of the query
-        *
-        * @private
-        *
-        * @param array $options an associative array of options to be turned into
-        *              an SQL query, valid keys are listed in the function.
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
-               $preLimitTail = $postLimitTail = '';
-               $startOpts = '';
-
-               $noKeyOptions = array();
-               foreach ( $options as $key => $option ) {
-                       if ( is_numeric( $key ) ) {
-                               $noKeyOptions[$option] = true;
-                       }
-               }
-
-               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
-               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
-
-               if (isset($options['LIMIT'])) {
-               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
-               //              isset($options['OFFSET']) ? $options['OFFSET']
-               //              : false);
-               }
-
-               #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
-               #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
-               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
-               if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
-                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
-               } else {
-                       $useIndex = '';
-               }
-
-               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
-       }
-
-       public function setTimeout( $timeout ) {
-               // @todo fixme no-op
-       }
-
-       function ping() {
-               wfDebug( "Function ping() not written for DatabaseOracle.php yet");
-               return true;
-       }
-
-       /**
-        * How lagged is this slave?
-        *
-        * @return int
-        */
-       public function getLag() {
-               # Not implemented for Oracle
-               return 0;
-       }
-
-       function setFakeSlaveLag() {}
-       function setFakeMaster() {}
-
-       function getDBname() {
-               return $this->mDBname;
-       }
-
-       function getServer() {
-               return $this->mServer;
-       }
-
-} // end DatabaseOracle class
diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php
deleted file mode 100644 (file)
index 8f8488b..0000000
+++ /dev/null
@@ -1,1330 +0,0 @@
-<?php
-/**
- * @ingroup Database
- * @file
- */
-
-/**
- * This is the Postgres database abstraction layer.
- *
- * As it includes more generic version for DB functions,
- * than MySQL ones, some of them should be moved to parent
- * Database class.
- *
- * @ingroup Database
- */
-class PostgresField {
-       private $name, $tablename, $type, $nullable, $max_length;
-
-       static function fromText($db, $table, $field) {
-       global $wgDBmwschema;
-
-               $q = <<<END
-SELECT
-CASE WHEN typname = 'int2' THEN 'smallint'
-WHEN typname = 'int4' THEN 'integer'
-WHEN typname = 'int8' THEN 'bigint'
-WHEN typname = 'bpchar' THEN 'char'
-ELSE typname END AS typname,
-attnotnull, attlen
-FROM pg_class, pg_namespace, pg_attribute, pg_type
-WHERE relnamespace=pg_namespace.oid
-AND relkind='r'
-AND attrelid=pg_class.oid
-AND atttypid=pg_type.oid
-AND nspname=%s
-AND relname=%s
-AND attname=%s;
-END;
-               $res = $db->query(sprintf($q,
-                               $db->addQuotes($wgDBmwschema),
-                               $db->addQuotes($table),
-                               $db->addQuotes($field)));
-               $row = $db->fetchObject($res);
-               if (!$row)
-                       return null;
-               $n = new PostgresField;
-               $n->type = $row->typname;
-               $n->nullable = ($row->attnotnull == 'f');
-               $n->name = $field;
-               $n->tablename = $table;
-               $n->max_length = $row->attlen;
-               return $n;
-       }
-
-       function name() {
-               return $this->name;
-       }
-
-       function tableName() {
-               return $this->tablename;
-       }
-
-       function type() {
-               return $this->type;
-       }
-
-       function nullable() {
-               return $this->nullable;
-       }
-
-       function maxLength() {
-               return $this->max_length;
-       }
-}
-
-/**
- * @ingroup Database
- */
-class DatabasePostgres extends Database {
-       var $mInsertId = NULL;
-       var $mLastResult = NULL;
-       var $numeric_version = NULL;
-
-       function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
-               $failFunction = false, $flags = 0 )
-       {
-
-               global $wgOut;
-               # Can't get a reference if it hasn't been set yet
-               if ( !isset( $wgOut ) ) {
-                       $wgOut = NULL;
-               }
-               $this->mOut =& $wgOut;
-               $this->mFailFunction = $failFunction;
-               $this->mFlags = $flags;
-               $this->open( $server, $user, $password, $dbName);
-
-       }
-
-       function cascadingDeletes() {
-               return true;
-       }
-       function cleanupTriggers() {
-               return true;
-       }
-       function strictIPs() {
-               return true;
-       }
-       function realTimestamps() {
-               return true;
-       }
-       function implicitGroupby() {
-               return false;
-       }
-       function implicitOrderby() {
-               return false;
-       }
-       function searchableIPs() {
-               return true;
-       }
-       function functionalIndexes() {
-               return true;
-       }
-
-       function hasConstraint( $name ) {
-               global $wgDBmwschema;
-               $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'";
-               return $this->numRows($res = $this->doQuery($SQL));
-       }
-
-       static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
-       {
-               return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
-       }
-
-       /**
-        * Usually aborts on failure
-        * If the failFunction is set to a non-zero integer, returns success
-        */
-       function open( $server, $user, $password, $dbName ) {
-               # Test for Postgres support, to avoid suppressed fatal error
-               if ( !function_exists( 'pg_connect' ) ) {
-                       throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
-               }
-
-               global $wgDBport;
-
-               if (!strlen($user)) { ## e.g. the class is being loaded
-                       return;
-               }
-
-               $this->close();
-               $this->mServer = $server;
-               $this->mPort = $port = $wgDBport;
-               $this->mUser = $user;
-               $this->mPassword = $password;
-               $this->mDBname = $dbName;
-
-               $hstring="";
-               if ($server!=false && $server!="") {
-                       $hstring="host=$server ";
-               }
-               if ($port!=false && $port!="") {
-                       $hstring .= "port=$port ";
-               }
-
-               error_reporting( E_ALL );
-               @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
-
-               if ( $this->mConn == false ) {
-                       wfDebug( "DB connection error\n" );
-                       wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
-                       wfDebug( $this->lastError()."\n" );
-                       return false;
-               }
-
-               $this->mOpened = true;
-
-               global $wgCommandLineMode;
-               ## If called from the command-line (e.g. importDump), only show errors
-               if ($wgCommandLineMode) {
-                       $this->doQuery("SET client_min_messages = 'ERROR'");
-               }
-
-               global $wgDBmwschema, $wgDBts2schema;
-               if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
-                       && $wgDBmwschema !== 'mediawiki'
-                       && preg_match( '/^\w+$/', $wgDBmwschema )
-                       && preg_match( '/^\w+$/', $wgDBts2schema )
-               ) {
-                       $safeschema = $this->quote_ident($wgDBmwschema);
-                       $safeschema2 = $this->quote_ident($wgDBts2schema);
-                       $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
-               }
-
-               return $this->mConn;
-       }
-
-
-       function initial_setup($password, $dbName) {
-               // If this is the initial connection, setup the schema stuff and possibly create the user
-               global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
-
-               print "<li>Checking the version of Postgres...";
-               $version = $this->getServerVersion();
-               $PGMINVER = '8.1';
-               if ($this->numeric_version < $PGMINVER) {
-                       print "<b>FAILED</b>. Required version is $PGMINVER. You have $this->numeric_version ($version)</li>\n";
-                       dieout("</ul>");
-               }
-               print "version $this->numeric_version is OK.</li>\n";
-
-               $safeuser = $this->quote_ident($wgDBuser);
-               // Are we connecting as a superuser for the first time?
-               if ($wgDBsuperuser) {
-                       // Are we really a superuser? Check out our rights
-                       $SQL = "SELECT
-                      CASE WHEN usesuper IS TRUE THEN
-                      CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
-                      ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
-                    END AS rights
-                    FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
-                       $rows = $this->numRows($res = $this->doQuery($SQL));
-                       if (!$rows) {
-                               print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n";
-                               dieout('</ul>');
-                       }
-                       $perms = pg_fetch_result($res, 0, 0);
-
-                       $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser);
-                       $rows = $this->numRows($this->doQuery($SQL));
-                       if ($rows) {
-                               print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>";
-                       }
-                       else {
-                               if ($perms != 1 and $perms != 3) {
-                                       print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. ";
-                                       print 'Please use a different Postgres user.</li>';
-                                       dieout('</ul>');
-                               }
-                               print "<li>Creating user <b>$wgDBuser</b>...";
-                               $safepass = $this->addQuotes($wgDBpassword);
-                               $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass";
-                               $this->doQuery($SQL);
-                               print "OK</li>\n";
-                       }
-                       // User now exists, check out the database
-                       if ($dbName != $wgDBname) {
-                               $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname);
-                               $rows = $this->numRows($this->doQuery($SQL));
-                               if ($rows) {
-                                       print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>";
-                               }
-                               else {
-                                       if ($perms < 2) {
-                                               print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. ";
-                                               print 'Please use a different Postgres user.</li>';
-                                               dieout('</ul>');
-                                       }
-                                       print "<li>Creating database <b>$wgDBname</b>...";
-                                       $safename = $this->quote_ident($wgDBname);
-                                       $SQL = "CREATE DATABASE $safename OWNER $safeuser ";
-                                       $this->doQuery($SQL);
-                                       print "OK</li>\n";
-                                       // Hopefully tsearch2 and plpgsql are in template1...
-                               }
-
-                               // Reconnect to check out tsearch2 rights for this user
-                               print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights...";
-
-                               $hstring="";
-                               if ($this->mServer!=false && $this->mServer!="") {
-                                       $hstring="host=$this->mServer ";
-                               }
-                               if ($this->mPort!=false && $this->mPort!="") {
-                                       $hstring .= "port=$this->mPort ";
-                               }
-
-                               @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$wgDBsuperuser password=$password");
-                               if ( $this->mConn == false ) {
-                                       print "<b>FAILED TO CONNECT!</b></li>";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>\n";
-                       }
-
-                       if ($this->numeric_version < 8.3) {
-                               // Tsearch2 checks
-                               print "<li>Checking that tsearch2 is installed in the database \"$wgDBname\"...";
-                               if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
-                                       print "<b>FAILED</b>. tsearch2 must be installed in the database \"$wgDBname\".";
-                                       print "Please see <a href='http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
-                                       print " for instructions or ask on #postgresql on irc.freenode.net</li>\n";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>\n";
-                               print "<li>Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables...";
-                               foreach (array('cfg','cfgmap','dict','parser') as $table) {
-                                       $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
-                                       $this->doQuery($SQL);
-                               }
-                               print "OK</li>\n";
-                       }
-
-                       // Setup the schema for this user if needed
-                       $result = $this->schemaExists($wgDBmwschema);
-                       $safeschema = $this->quote_ident($wgDBmwschema);
-                       if (!$result) {
-                               print "<li>Creating schema <b>$wgDBmwschema</b> ...";
-                               $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser");
-                               if (!$result) {
-                                       print "<b>FAILED</b>.</li>\n";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>\n";
-                       }
-                       else {
-                               print "<li>Schema already exists, explicitly granting rights...\n";
-                               $safeschema2 = $this->addQuotes($wgDBmwschema);
-                               $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
-                                       "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
-                                       "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
-                                       "AND p.relkind IN ('r','S','v')\n";
-                               $SQL .= "UNION\n";
-                               $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
-                                       "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
-                                       "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
-                                       "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
-                               $res = $this->doQuery($SQL);
-                               if (!$res) {
-                                       print "<b>FAILED</b>. Could not set rights for the user.</li>\n";
-                                       dieout("</ul>");
-                               }
-                               $this->doQuery("SET search_path = $safeschema");
-                               $rows = $this->numRows($res);
-                               while ($rows) {
-                                       $rows--;
-                                       $this->doQuery(pg_fetch_result($res, $rows, 0));
-                               }
-                               print "OK</li>";
-                       }
-
-                       // Install plpgsql if needed
-                       $this->setup_plpgsql();
-
-                       $wgDBsuperuser = '';
-                       return true; // Reconnect as regular user
-
-               } // end superuser
-
-               if (!defined('POSTGRES_SEARCHPATH')) {
-
-                       if ($this->numeric_version < 8.3) {
-                               // Do we have the basic tsearch2 table?
-                               print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"...";
-                               if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
-                                       print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href=";
-                                       print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
-                                       print " for instructions.</li>\n";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>\n";
-
-                               // Does this user have the rights to the tsearch2 tables?
-                               $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
-                               print "<li>Checking tsearch2 permissions...";
-                               // Let's check all four, just to be safe
-                               error_reporting( 0 );
-                               $ts2tables = array('cfg','cfgmap','dict','parser');
-                               $safetsschema = $this->quote_ident($wgDBts2schema);
-                               foreach ( $ts2tables AS $tname ) {
-                                       $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
-                                       $res = $this->doQuery($SQL);
-                                       if (!$res) {
-                                               print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ".
-                                                       "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n";
-                                               dieout("</ul>");
-                                       }
-                               }
-                               $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'";
-                               $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
-                               $res = $this->doQuery($SQL);
-                               error_reporting( E_ALL );
-                               if (!$res) {
-                                       print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>";
-
-                               // Will the current locale work? Can we force it to?
-                               print "<li>Verifying tsearch2 locale with $ctype...";
-                               $rows = $this->numRows($res);
-                               $resetlocale = 0;
-                               if (!$rows) {
-                                       print "<b>not found</b></li>\n";
-                                       print "<li>Attempting to set default tsearch2 locale to \"$ctype\"...";
-                                       $resetlocale = 1;
-                               }
-                               else {
-                                       $tsname = pg_fetch_result($res, 0, 0);
-                                       if ($tsname != 'default') {
-                                               print "<b>not set to default ($tsname)</b>";
-                                               print "<li>Attempting to change tsearch2 default locale to \"$ctype\"...";
-                                               $resetlocale = 1;
-                                       }
-                               }
-                               if ($resetlocale) {
-                                       $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'";
-                                       $res = $this->doQuery($SQL);
-                                       if (!$res) {
-                                               print "<b>FAILED</b>. ";
-                                               print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n";
-                                               dieout("</ul>");
-                                       }
-                                       print "OK</li>";
-                               }
-
-                               // Final test: try out a simple tsearch2 query
-                               $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
-                               $res = $this->doQuery($SQL);
-                               if (!$res) {
-                                       print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>";
-                       }
-
-                       // Install plpgsql if needed
-                       $this->setup_plpgsql();
-
-                       // Does the schema already exist? Who owns it?
-                       $result = $this->schemaExists($wgDBmwschema);
-                       if (!$result) {
-                               print "<li>Creating schema <b>$wgDBmwschema</b> ...";
-                               error_reporting( 0 );
-                               $safeschema = $this->quote_ident($wgDBmwschema);
-                               $result = $this->doQuery("CREATE SCHEMA $safeschema");
-                               error_reporting( E_ALL );
-                               if (!$result) {
-                                       print "<b>FAILED</b>. The user \"$wgDBuser\" must be able to access the schema. ".
-                                               "You can try making them the owner of the database, or try creating the schema with a ".
-                                               "different user, and then grant access to the \"$wgDBuser\" user.</li>\n";
-                                       dieout("</ul>");
-                               }
-                               print "OK</li>\n";
-                       }
-                       else if ($result != $wgDBuser) {
-                               print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.</li>\n";
-                       }
-                       else {
-                               print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.</li>\n";
-                       }
-
-                       // Always return GMT time to accomodate the existing integer-based timestamp assumption
-                       print "<li>Setting the timezone to GMT for user \"$wgDBuser\" ...";
-                       $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
-                       $result = pg_query($this->mConn, $SQL);
-                       if (!$result) {
-                               print "<b>FAILED</b>.</li>\n";
-                               dieout("</ul>");
-                       }
-                       print "OK</li>\n";
-                       // Set for the rest of this session
-                       $SQL = "SET timezone = 'GMT'";
-                       $result = pg_query($this->mConn, $SQL);
-                       if (!$result) {
-                               print "<li>Failed to set timezone</li>\n";
-                               dieout("</ul>");
-                       }
-
-                       print "<li>Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ...";
-                       $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
-                       $result = pg_query($this->mConn, $SQL);
-                       if (!$result) {
-                               print "<b>FAILED</b>.</li>\n";
-                               dieout("</ul>");
-                       }
-                       print "OK</li>\n";
-                       // Set for the rest of this session
-                       $SQL = "SET datestyle = 'ISO, YMD'";
-                       $result = pg_query($this->mConn, $SQL);
-                       if (!$result) {
-                               print "<li>Failed to set datestyle</li>\n";
-                               dieout("</ul>");
-                       }
-
-                       // Fix up the search paths if needed
-                       print "<li>Setting the search path for user \"$wgDBuser\" ...";
-                       $path = $this->quote_ident($wgDBmwschema);
-                       if ($wgDBts2schema !== $wgDBmwschema)
-                               $path .= ", ". $this->quote_ident($wgDBts2schema);
-                       if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
-                               $path .= ", public";
-                       $SQL = "ALTER USER $safeuser SET search_path = $path";
-                       $result = pg_query($this->mConn, $SQL);
-                       if (!$result) {
-                               print "<b>FAILED</b>.</li>\n";
-                               dieout("</ul>");
-                       }
-                       print "OK</li>\n";
-                       // Set for the rest of this session
-                       $SQL = "SET search_path = $path";
-                       $result = pg_query($this->mConn, $SQL);
-                       if (!$result) {
-                               print "<li>Failed to set search_path</li>\n";
-                               dieout("</ul>");
-                       }
-                       define( "POSTGRES_SEARCHPATH", $path );
-               }
-       }
-
-
-       function setup_plpgsql() {
-               print "<li>Checking for Pl/Pgsql ...";
-               $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
-               $rows = $this->numRows($this->doQuery($SQL));
-               if ($rows < 1) {
-                       // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
-                       print "not installed. Attempting to install Pl/Pgsql ...";
-                       $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
-                               "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
-                       $rows = $this->numRows($this->doQuery($SQL));
-                       if ($rows >= 1) {
-                       $olde = error_reporting(0);
-                               error_reporting($olde - E_WARNING);
-                               $result = $this->doQuery("CREATE LANGUAGE plpgsql");
-                               error_reporting($olde);
-                               if (!$result) {
-                                       print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
-                                       dieout("</ul>");
-                               }
-                       }
-                       else {
-                               print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
-                               dieout("</ul>");
-                       }
-               }
-               print "OK</li>\n";
-       }
-
-
-       /**
-        * Closes a database connection, if it is open
-        * Returns success, true if already closed
-        */
-       function close() {
-               $this->mOpened = false;
-               if ( $this->mConn ) {
-                       return pg_close( $this->mConn );
-               } else {
-                       return true;
-               }
-       }
-
-       function doQuery( $sql ) {
-               if (function_exists('mb_convert_encoding')) {
-                       return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') );
-               }
-               return $this->mLastResult=pg_query( $this->mConn , $sql);
-       }
-
-       function queryIgnore( $sql, $fname = '' ) {
-               return $this->query( $sql, $fname, true );
-       }
-
-       function freeResult( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               if ( !@pg_free_result( $res ) ) {
-                       throw new DBUnexpectedError($this,  "Unable to free Postgres result\n" );
-               }
-       }
-
-       function fetchObject( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @$row = pg_fetch_object( $res );
-               # FIXME: HACK HACK HACK HACK debug
-
-               # TODO:
-               # hashar : not sure if the following test really trigger if the object
-               #          fetching failed.
-               if( pg_last_error($this->mConn) ) {
-                       throw new DBUnexpectedError($this,  'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
-               }
-               return $row;
-       }
-
-       function fetchRow( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @$row = pg_fetch_array( $res );
-               if( pg_last_error($this->mConn) ) {
-                       throw new DBUnexpectedError($this,  'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
-               }
-               return $row;
-       }
-
-       function numRows( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               @$n = pg_num_rows( $res );
-               if( pg_last_error($this->mConn) ) {
-                       throw new DBUnexpectedError($this,  'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
-               }
-               return $n;
-       }
-       function numFields( $res ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return pg_num_fields( $res );
-       }
-       function fieldName( $res, $n ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return pg_field_name( $res, $n );
-       }
-
-       /**
-        * This must be called after nextSequenceVal
-        */
-       function insertId() {
-               return $this->mInsertId;
-       }
-
-       function dataSeek( $res, $row ) {
-               if ( $res instanceof ResultWrapper ) {
-                       $res = $res->result;
-               }
-               return pg_result_seek( $res, $row );
-       }
-
-       function lastError() {
-               if ( $this->mConn ) {
-                       return pg_last_error();
-               }
-               else {
-                       return "No database connection";
-               }
-       }
-       function lastErrno() {
-               return pg_last_error() ? 1 : 0;
-       }
-
-       function affectedRows() {
-               if( !isset( $this->mLastResult ) or ! $this->mLastResult )
-                       return 0;
-
-               return pg_affected_rows( $this->mLastResult );
-       }
-
-       /**
-        * Estimate rows in dataset
-        * Returns estimated count, based on EXPLAIN output
-        * This is not necessarily an accurate estimate, so use sparingly
-        * Returns -1 if count cannot be found
-        * Takes same arguments as Database::select()
-        */
-
-       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
-               $options['EXPLAIN'] = true;
-               $res = $this->select( $table, $vars, $conds, $fname, $options );
-               $rows = -1;
-               if ( $res ) {
-                       $row = $this->fetchRow( $res );
-                       $count = array();
-                       if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
-                               $rows = $count[1];
-                       }
-                       $this->freeResult($res);
-               }
-               return $rows;
-       }
-
-
-       /**
-        * Returns information about an index
-        * If errors are explicitly ignored, returns NULL on failure
-        */
-       function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
-               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
-               $res = $this->query( $sql, $fname );
-               if ( !$res ) {
-                       return NULL;
-               }
-               while ( $row = $this->fetchObject( $res ) ) {
-                       if ( $row->indexname == $index ) {
-                               return $row;
-                       }
-               }
-               return false;
-       }
-
-       function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
-               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
-                       " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
-               $res = $this->query( $sql, $fname );
-               if ( !$res )
-                       return NULL;
-               while ($row = $this->fetchObject( $res ))
-                       return true;
-               return false;
-
-       }
-
-       /**
-        * INSERT wrapper, inserts an array into a table
-        *
-        * $args may be a single associative array, or an array of these with numeric keys,
-        * for multi-row insert (Postgres version 8.2 and above only).
-        *
-        * @param array $table   String: Name of the table to insert to.
-        * @param array $args    Array: Items to insert into the table.
-        * @param array $fname   String: Name of the function, for profiling
-        * @param mixed $options String or Array. Valid options: IGNORE
-        *
-        * @return bool Success of insert operation. IGNORE always returns true.
-        */
-       function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
-               global $wgDBversion;
-
-               if ( !count( $args ) ) {
-                       return true;
-               }
-
-               $table = $this->tableName( $table );
-               if (! isset( $wgDBversion ) ) {
-                       $this->getServerVersion();
-                       $wgDBversion = $this->numeric_version;
-               }
-
-               if ( !is_array( $options ) )
-                       $options = array( $options );
-
-               if ( isset( $args[0] ) && is_array( $args[0] ) ) {
-                       $multi = true;
-                       $keys = array_keys( $args[0] );
-               }
-               else {
-                       $multi = false;
-                       $keys = array_keys( $args );
-               }
-
-               $ignore = in_array( 'IGNORE', $options ) ? 1 : 0;
-               if ( $ignore )
-                       $olde = error_reporting( 0 );
-
-               $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
-
-               if ( $multi ) {
-                       if ( $wgDBversion >= 8.2 ) {
-                               $first = true;
-                               foreach ( $args as $row ) {
-                                       if ( $first ) {
-                                               $first = false;
-                                       } else {
-                                               $sql .= ',';
-                                       }
-                                       $sql .= '(' . $this->makeList( $row ) . ')';
-                               }
-                               $res = (bool)$this->query( $sql, $fname, $ignore );
-                       }
-                       else {
-                               $res = true;
-                               $origsql = $sql;
-                               foreach ( $args as $row ) {
-                                       $tempsql = $origsql;
-                                       $tempsql .= '(' . $this->makeList( $row ) . ')';
-                                       $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
-                                       if (! $tempres)
-                                               $res = false;
-                               }
-                       }
-               }
-               else {
-                       $sql .= '(' . $this->makeList( $args ) . ')';
-                       $res = (bool)$this->query( $sql, $fname, $ignore );
-               }
-
-               if ( $ignore ) {
-                       $olde = error_reporting( $olde );
-                       return true;
-               }
-
-               return $res;
-
-       }
-
-       function tableName( $name ) {
-               # Replace reserved words with better ones
-               switch( $name ) {
-                       case 'user':
-                               return 'mwuser';
-                       case 'text':
-                               return 'pagecontent';
-                       default:
-                               return $name;
-               }
-       }
-
-       /**
-        * Return the next in a sequence, save the value for retrieval via insertId()
-        */
-       function nextSequenceValue( $seqName ) {
-               $safeseq = preg_replace( "/'/", "''", $seqName );
-               $res = $this->query( "SELECT nextval('$safeseq')" );
-               $row = $this->fetchRow( $res );
-               $this->mInsertId = $row[0];
-               $this->freeResult( $res );
-               return $this->mInsertId;
-       }
-
-       /**
-        * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
-        */
-       function currentSequenceValue( $seqName ) {
-               $safeseq = preg_replace( "/'/", "''", $seqName );
-               $res = $this->query( "SELECT currval('$safeseq')" );
-               $row = $this->fetchRow( $res );
-               $currval = $row[0];
-               $this->freeResult( $res );
-               return $currval;
-       }
-
-       /**
-        * Postgres does not have a "USE INDEX" clause, so return an empty string
-        */
-       function useIndexClause( $index ) {
-               return '';
-       }
-
-       # REPLACE query wrapper
-       # Postgres simulates this with a DELETE followed by INSERT
-       # $row is the row to insert, an associative array
-       # $uniqueIndexes is an array of indexes. Each element may be either a
-       # field name or an array of field names
-       #
-       # It may be more efficient to leave off unique indexes which are unlikely to collide.
-       # However if you do this, you run the risk of encountering errors which wouldn't have
-       # occurred in MySQL
-       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
-               $table = $this->tableName( $table );
-
-               if (count($rows)==0) {
-                       return;
-               }
-
-               # Single row case
-               if ( !is_array( reset( $rows ) ) ) {
-                       $rows = array( $rows );
-               }
-
-               foreach( $rows as $row ) {
-                       # Delete rows which collide
-                       if ( $uniqueIndexes ) {
-                               $sql = "DELETE FROM $table WHERE ";
-                               $first = true;
-                               foreach ( $uniqueIndexes as $index ) {
-                                       if ( $first ) {
-                                               $first = false;
-                                               $sql .= "(";
-                                       } else {
-                                               $sql .= ') OR (';
-                                       }
-                                       if ( is_array( $index ) ) {
-                                               $first2 = true;
-                                               foreach ( $index as $col ) {
-                                                       if ( $first2 ) {
-                                                               $first2 = false;
-                                                       } else {
-                                                               $sql .= ' AND ';
-                                                       }
-                                                       $sql .= $col.'=' . $this->addQuotes( $row[$col] );
-                                               }
-                                       } else {
-                                               $sql .= $index.'=' . $this->addQuotes( $row[$index] );
-                                       }
-                               }
-                               $sql .= ')';
-                               $this->query( $sql, $fname );
-                       }
-
-                       # Now insert the row
-                       $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
-                               $this->makeList( $row, LIST_COMMA ) . ')';
-                       $this->query( $sql, $fname );
-               }
-       }
-
-       # DELETE where the condition is a join
-       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
-               if ( !$conds ) {
-                       throw new DBUnexpectedError($this,  'Database::deleteJoin() called with empty $conds' );
-               }
-
-               $delTable = $this->tableName( $delTable );
-               $joinTable = $this->tableName( $joinTable );
-               $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
-               if ( $conds != '*' ) {
-                       $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
-               }
-               $sql .= ')';
-
-               $this->query( $sql, $fname );
-       }
-
-       # Returns the size of a text field, or -1 for "unlimited"
-       function textFieldSize( $table, $field ) {
-               $table = $this->tableName( $table );
-               $sql = "SELECT t.typname as ftype,a.atttypmod as size
-                       FROM pg_class c, pg_attribute a, pg_type t
-                       WHERE relname='$table' AND a.attrelid=c.oid AND
-                               a.atttypid=t.oid and a.attname='$field'";
-               $res =$this->query($sql);
-               $row=$this->fetchObject($res);
-               if ($row->ftype=="varchar") {
-                       $size=$row->size-4;
-               } else {
-                       $size=$row->size;
-               }
-               $this->freeResult( $res );
-               return $size;
-       }
-
-       function lowPriorityOption() {
-               return '';
-       }
-
-       function limitResult($sql, $limit, $offset=false) {
-               return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
-       }
-
-       /**
-        * Returns an SQL expression for a simple conditional.
-        * Uses CASE on Postgres
-        *
-        * @param string $cond SQL expression which will result in a boolean value
-        * @param string $trueVal SQL expression to return if true
-        * @param string $falseVal SQL expression to return if false
-        * @return string SQL fragment
-        */
-       function conditional( $cond, $trueVal, $falseVal ) {
-               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
-       }
-
-       function wasDeadlock() {
-               return $this->lastErrno() == '40P01';
-       }
-
-       function timestamp( $ts=0 ) {
-               return wfTimestamp(TS_POSTGRES,$ts);
-       }
-
-       /**
-        * Return aggregated value function call
-        */
-       function aggregateValue ($valuedata,$valuename='value') {
-               return $valuedata;
-       }
-
-
-       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               // Ignore errors during error handling to avoid infinite recursion
-               $ignore = $this->ignoreErrors( true );
-               $this->mErrorCount++;
-
-               if ($ignore || $tempIgnore) {
-                       wfDebug("SQL ERROR (ignored): $error\n");
-                       $this->ignoreErrors( $ignore );
-               }
-               else {
-                       $message = "A database error has occurred\n" .
-                               "Query: $sql\n" .
-                               "Function: $fname\n" .
-                               "Error: $errno $error\n";
-                       throw new DBUnexpectedError($this, $message);
-               }
-       }
-
-       /**
-        * @return string wikitext of a link to the server software's web site
-        */
-               function getSoftwareLink() {
-               return "[http://www.postgresql.org/ PostgreSQL]";
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0);
-               $thisver = array();
-               if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) {
-                       die("Could not determine the numeric version from $version!");
-               }
-               $this->numeric_version = $thisver[1];
-               return $version;
-       }
-
-
-       /**
-        * Query whether a given relation exists (in the given schema, or the
-        * default mw one if not given)
-        */
-       function relationExists( $table, $types, $schema = false ) {
-               global $wgDBmwschema;
-               if (!is_array($types))
-                       $types = array($types);
-               if (! $schema )
-                       $schema = $wgDBmwschema;
-               $etable = $this->addQuotes($table);
-               $eschema = $this->addQuotes($schema);
-               $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
-                       . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
-                       . "AND c.relkind IN ('" . implode("','", $types) . "')";
-               $res = $this->query( $SQL );
-               $count = $res ? $res->numRows() : 0;
-               if ($res)
-                       $this->freeResult( $res );
-               return $count ? true : false;
-       }
-
-       /*
-        * For backward compatibility, this function checks both tables and
-        * views.
-        */
-       function tableExists ($table, $schema = false) {
-               return $this->relationExists($table, array('r', 'v'), $schema);
-       }
-
-       function sequenceExists ($sequence, $schema = false) {
-               return $this->relationExists($sequence, 'S', $schema);
-       }
-
-       function triggerExists($table, $trigger) {
-               global $wgDBmwschema;
-
-               $q = <<<END
-       SELECT 1 FROM pg_class, pg_namespace, pg_trigger
-               WHERE relnamespace=pg_namespace.oid AND relkind='r'
-                     AND tgrelid=pg_class.oid
-                     AND nspname=%s AND relname=%s AND tgname=%s
-END;
-               $res = $this->query(sprintf($q,
-                               $this->addQuotes($wgDBmwschema),
-                               $this->addQuotes($table),
-                               $this->addQuotes($trigger)));
-               if (!$res)
-                       return NULL;
-               $rows = $res->numRows();
-               $this->freeResult($res);
-               return $rows;
-       }
-
-       function ruleExists($table, $rule) {
-               global $wgDBmwschema;
-               $exists = $this->selectField("pg_rules", "rulename",
-                               array(  "rulename" => $rule,
-                                       "tablename" => $table,
-                                       "schemaname" => $wgDBmwschema));
-               return $exists === $rule;
-       }
-
-       function constraintExists($table, $constraint) {
-               global $wgDBmwschema;
-               $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
-                          "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
-                       $this->addQuotes($wgDBmwschema),
-                       $this->addQuotes($table),
-                       $this->addQuotes($constraint));
-               $res = $this->query($SQL);
-               if (!$res)
-                       return NULL;
-               $rows = $res->numRows();
-               $this->freeResult($res);
-               return $rows;
-       }
-
-       /**
-        * Query whether a given schema exists. Returns the name of the owner
-        */
-       function schemaExists( $schema ) {
-               $eschema = preg_replace("/'/", "''", $schema);
-               $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
-                               ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
-               $res = $this->query( $SQL );
-               if ( $res && $res->numRows() ) {
-                       $row = $res->fetchObject();
-                       $owner = $row->rolname;
-               } else {
-                       $owner = false;
-               }
-               if ($res)
-                       $this->freeResult($res);
-               return $owner;
-       }
-
-       /**
-        * Query whether a given column exists in the mediawiki schema
-        */
-       function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
-               global $wgDBmwschema;
-               $etable = preg_replace("/'/", "''", $table);
-               $eschema = preg_replace("/'/", "''", $wgDBmwschema);
-               $ecol = preg_replace("/'/", "''", $field);
-               $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
-                       . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
-                       . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
-               $res = $this->query( $SQL, $fname );
-               $count = $res ? $res->numRows() : 0;
-               if ($res)
-                       $this->freeResult( $res );
-               return $count;
-       }
-
-       function fieldInfo( $table, $field ) {
-               return PostgresField::fromText($this, $table, $field);
-       }
-
-       function begin( $fname = 'DatabasePostgres::begin' ) {
-               $this->query( 'BEGIN', $fname );
-               $this->mTrxLevel = 1;
-       }
-       function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
-               return true;
-       }
-       function commit( $fname = 'DatabasePostgres::commit' ) {
-               $this->query( 'COMMIT', $fname );
-               $this->mTrxLevel = 0;
-       }
-
-       /* Not even sure why this is used in the main codebase... */
-       function limitResultForUpdate($sql, $num) {
-               return $sql;
-       }
-
-       function setup_database() {
-               global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
-
-               // Make sure that we can write to the correct schema
-               // If not, Postgres will happily and silently go to the next search_path item
-               $ctest = "mediawiki_test_table";
-               $safeschema = $this->quote_ident($wgDBmwschema);
-               if ($this->tableExists($ctest, $wgDBmwschema)) {
-                       $this->doQuery("DROP TABLE $safeschema.$ctest");
-               }
-               $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
-               $olde = error_reporting( 0 );
-               $res = $this->doQuery($SQL);
-               error_reporting( $olde );
-               if (!$res) {
-                       print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n";
-                       dieout("</ul>");
-               }
-               $this->doQuery("DROP TABLE $safeschema.$ctest");
-
-               $res = dbsource( "../maintenance/postgres/tables.sql", $this);
-
-               ## Update version information
-               $mwv = $this->addQuotes($wgVersion);
-               $pgv = $this->addQuotes($this->getServerVersion());
-               $pgu = $this->addQuotes($this->mUser);
-               $mws = $this->addQuotes($wgDBmwschema);
-               $tss = $this->addQuotes($wgDBts2schema);
-               $pgp = $this->addQuotes($wgDBport);
-               $dbn = $this->addQuotes($this->mDBname);
-               $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
-
-               $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
-                               "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
-                               "ctype = '$ctype' ".
-                               "WHERE type = 'Creation'";
-               $this->query($SQL);
-
-               ## Avoid the non-standard "REPLACE INTO" syntax
-               $f = fopen( "../maintenance/interwiki.sql", 'r' );
-               if ($f == false ) {
-                       dieout( "<li>Could not find the interwiki.sql file");
-               }
-               ## We simply assume it is already empty as we have just created it
-               $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
-               while ( ! feof( $f ) ) {
-                       $line = fgets($f,1024);
-                       $matches = array();
-                       if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
-                               continue;
-                       }
-                       $this->query("$SQL $matches[1],$matches[2])");
-               }
-               print " (table interwiki successfully populated)...\n";
-
-               $this->doQuery("COMMIT");
-       }
-
-       function encodeBlob( $b ) {
-               return new Blob ( pg_escape_bytea( $b ) ) ;
-       }
-
-       function decodeBlob( $b ) {
-               if ($b instanceof Blob) {
-                       $b = $b->fetch();
-               }
-               return pg_unescape_bytea( $b );
-       }
-
-       function strencode( $s ) { ## Should not be called by us
-               return pg_escape_string( $s );
-       }
-
-       function addQuotes( $s ) {
-               if ( is_null( $s ) ) {
-                       return 'NULL';
-               } else if ($s instanceof Blob) {
-                       return "'".$s->fetch($s)."'";
-               }
-               return "'" . pg_escape_string($s) . "'";
-       }
-
-       function quote_ident( $s ) {
-               return '"' . preg_replace( '/"/', '""', $s) . '"';
-       }
-
-       /* For now, does nothing */
-       function selectDB( $db ) {
-               return true;
-       }
-
-       /**
-        * Postgres specific version of replaceVars.
-        * Calls the parent version in Database.php
-        *
-        * @private
-        *
-        * @param string $com SQL string, read from a stream (usually tables.sql)
-        *
-        * @return string SQL string
-        */
-       protected function replaceVars( $ins ) {
-
-               $ins = parent::replaceVars( $ins );
-
-               if ($this->numeric_version >= 8.3) {
-                       // Thanks for not providing backwards-compatibility, 8.3
-                       $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
-               }
-
-               if ($this->numeric_version <= 8.1) { // Our minimum version
-                       $ins = str_replace( 'USING gin', 'USING gist', $ins );
-               }
-
-               return $ins;
-       }
-
-       /**
-        * Various select options
-        *
-        * @private
-        *
-        * @param array $options an associative array of options to be turned into
-        *              an SQL query, valid keys are listed in the function.
-        * @return array
-        */
-       function makeSelectOptions( $options ) {
-               $preLimitTail = $postLimitTail = '';
-               $startOpts = $useIndex = '';
-
-               $noKeyOptions = array();
-               foreach ( $options as $key => $option ) {
-                       if ( is_numeric( $key ) ) {
-                               $noKeyOptions[$option] = true;
-                       }
-               }
-
-               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
-               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
-               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
-
-               //if (isset($options['LIMIT'])) {
-               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
-               //              isset($options['OFFSET']) ? $options['OFFSET']
-               //              : false);
-               //}
-
-               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
-               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
-               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
-
-               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
-       }
-
-       public function setTimeout( $timeout ) {
-               // @todo fixme no-op
-       }
-
-       function ping() {
-               wfDebug( "Function ping() not written for DatabasePostgres.php yet");
-               return true;
-       }
-
-       /**
-        * How lagged is this slave?
-        *
-        */
-       public function getLag() {
-               # Not implemented for PostgreSQL
-               return false;
-       }
-
-       function setFakeSlaveLag() {}
-       function setFakeMaster() {}
-
-       function getDBname() {
-               return $this->mDBname;
-       }
-
-       function getServer() {
-               return $this->mServer;
-       }
-
-       function buildConcat( $stringList ) {
-               return implode( ' || ', $stringList );
-       }
-
-} // end DatabasePostgres class
diff --git a/includes/DatabaseSqlite.php b/includes/DatabaseSqlite.php
deleted file mode 100644 (file)
index 8b4466e..0000000
+++ /dev/null
@@ -1,395 +0,0 @@
-<?php
-/**
- * This script is the SQLite database abstraction layer
- *
- * See maintenance/sqlite/README for development notes and other specific information
- * @ingroup Database
- * @file
- */
-
-/**
- * @ingroup Database
- */
-class DatabaseSqlite extends Database {
-
-       var $mAffectedRows;
-       var $mLastResult;
-       var $mDatabaseFile;
-
-       /**
-        * Constructor
-        */
-       function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) {
-               global $wgOut,$wgSQLiteDataDir;
-               if ("$wgSQLiteDataDir" == '') $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data';
-               if (!is_dir($wgSQLiteDataDir)) mkdir($wgSQLiteDataDir,0700);
-               if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet
-               $this->mOut =& $wgOut;
-               $this->mFailFunction = $failFunction;
-               $this->mFlags = $flags;
-               $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
-               $this->open($server, $user, $password, $dbName);
-       }
-
-       /**
-        * todo: check if these should be true like parent class
-        */
-       function implicitGroupby()   { return false; }
-       function implicitOrderby()   { return false; }
-
-       static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
-               return new DatabaseSqlite($server, $user, $password, $dbName, $failFunction, $flags);
-       }
-
-       /** Open an SQLite database and return a resource handle to it
-        *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
-        */
-       function open($server,$user,$pass,$dbName) {
-               $this->mConn = false;
-               if ($dbName) {
-                       $file = $this->mDatabaseFile;
-                       if ($this->mFlags & DBO_PERSISTENT) $this->mConn = new PDO("sqlite:$file",$user,$pass,array(PDO::ATTR_PERSISTENT => true));
-                       else $this->mConn = new PDO("sqlite:$file",$user,$pass);
-                       if ($this->mConn === false) wfDebug("DB connection error: $err\n");;
-                       $this->mOpened = $this->mConn;
-                       $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions
-               }
-               return $this->mConn;
-       }
-
-       /**
-        * Close an SQLite database
-        */
-       function close() {
-               $this->mOpened = false;
-               if (is_object($this->mConn)) {
-                       if ($this->trxLevel()) $this->immediateCommit();
-                       $this->mConn = null;
-               }
-               return true;
-       }
-
-       /**
-        * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
-        */
-       function doQuery($sql) {
-               $res = $this->mConn->query($sql);
-               if ($res === false) $this->reportQueryError($this->lastError(),$this->lastErrno(),$sql,__FUNCTION__);
-               else {
-                       $r = $res instanceof ResultWrapper ? $res->result : $res;
-                       $this->mAffectedRows = $r->rowCount();
-                       $res = new ResultWrapper($this,$r->fetchAll());
-               }
-               return $res;
-       }
-
-       function freeResult(&$res) {
-               if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
-       }
-
-       function fetchObject(&$res) {
-               if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
-               $cur = current($r);
-               if (is_array($cur)) {
-                       next($r);
-                       $obj = new stdClass;
-                       foreach ($cur as $k => $v) if (!is_numeric($k)) $obj->$k = $v;
-                       return $obj;
-               }
-               return false;
-       }
-
-       function fetchRow(&$res) {
-               if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
-               $cur = current($r);
-               if (is_array($cur)) {
-                       next($r);
-                       return $cur;
-               }
-               return false;
-       }
-
-       /**
-        * The PDO::Statement class implements the array interface so count() will work
-        */
-       function numRows(&$res) {
-               $r = $res instanceof ResultWrapper ? $res->result : $res;
-               return count($r);
-       }
-
-       function numFields(&$res) {
-               $r = $res instanceof ResultWrapper ? $res->result : $res;
-               return is_array($r) ? count($r[0]) : 0;
-       }
-
-       function fieldName(&$res,$n) {
-               $r = $res instanceof ResultWrapper ? $res->result : $res;
-               if (is_array($r)) {
-                       $keys = array_keys($r[0]);
-                       return $keys[$n];
-               }
-               return  false;
-       }
-
-       /**
-        * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
-        */
-       function tableName($name) {
-               return str_replace('`','',parent::tableName($name));
-       }
-
-       /**
-        * This must be called after nextSequenceVal
-        */
-       function insertId() {
-               return $this->mConn->lastInsertId();
-       }
-
-       function dataSeek(&$res,$row) {
-               if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
-               reset($r);
-               if ($row > 0) for ($i = 0; $i < $row; $i++) next($r);
-       }
-
-       function lastError() {
-               if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
-               $e = $this->mConn->errorInfo();
-               return isset($e[2]) ? $e[2] : '';
-       }
-
-       function lastErrno() {
-               if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
-               return $this->mConn->errorCode();
-       }
-
-       function affectedRows() {
-               return $this->mAffectedRows;
-       }
-
-       /**
-        * Returns information about an index
-        * - if errors are explicitly ignored, returns NULL on failure
-        */
-       function indexInfo($table, $index, $fname = 'Database::indexExists') {
-               return false;
-       }
-
-       function indexUnique($table, $index, $fname = 'Database::indexUnique') {
-               return false;
-       }
-
-       /**
-        * Filter the options used in SELECT statements
-        */
-       function makeSelectOptions($options) {
-               foreach ($options as $k => $v) if (is_numeric($k) && $v == 'FOR UPDATE') $options[$k] = '';
-               return parent::makeSelectOptions($options);
-       }
-
-       /**
-        * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments
-        */
-       function insert($table, $a, $fname = 'DatabaseSqlite::insert', $options = array()) {
-               if (!count($a)) return true;
-               if (!is_array($options)) $options = array($options);
-
-               # SQLite uses OR IGNORE not just IGNORE
-               foreach ($options as $k => $v) if ($v == 'IGNORE') $options[$k] = 'OR IGNORE';
-
-               # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
-               if (isset($a[0]) && is_array($a[0])) {
-                       $ret = true;
-                       foreach ($a as $k => $v) if (!parent::insert($table,$v,"$fname/multi-row",$options)) $ret = false;
-               }
-               else $ret = parent::insert($table,$a,"$fname/single-row",$options);
-
-               return $ret;
-       }
-
-       /**
-        * SQLite does not have a "USE INDEX" clause, so return an empty string
-        */
-       function useIndexClause($index) {
-               return '';
-       }
-
-       # Returns the size of a text field, or -1 for "unlimited"
-       function textFieldSize($table, $field) {
-               return -1;
-       }
-
-       /**
-        * No low priority option in SQLite
-        */
-       function lowPriorityOption() {
-               return '';
-       }
-
-       /**
-        * Returns an SQL expression for a simple conditional.
-        * - uses CASE on SQLite
-        */
-       function conditional($cond, $trueVal, $falseVal) {
-               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
-       }
-
-       function wasDeadlock() {
-               return $this->lastErrno() == SQLITE_BUSY;
-       }
-
-       /**
-        * @return string wikitext of a link to the server software's web site
-        */
-       function getSoftwareLink() {
-               return "[http://sqlite.org/ SQLite]";
-       }
-
-       /**
-        * @return string Version information from the database
-        */
-       function getServerVersion() {
-               global $wgContLang;
-               $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
-               $size = $wgContLang->formatSize(filesize($this->mDatabaseFile));
-               $file = basename($this->mDatabaseFile);
-               return $ver." ($file: $size)";
-       }
-
-       /**
-        * Query whether a given column exists in the mediawiki schema
-        */
-       function fieldExists($table, $field) { return true; }
-
-       function fieldInfo($table, $field) { return SQLiteField::fromText($this, $table, $field); }
-
-       function begin() {
-               if ($this->mTrxLevel == 1) $this->commit();
-               $this->mConn->beginTransaction();
-               $this->mTrxLevel = 1;
-       }
-
-       function commit() {
-               if ($this->mTrxLevel == 0) return;
-               $this->mConn->commit();
-               $this->mTrxLevel = 0;
-       }
-
-       function rollback() {
-               if ($this->mTrxLevel == 0) return;
-               $this->mConn->rollBack();
-               $this->mTrxLevel = 0;
-       }
-
-       function limitResultForUpdate($sql, $num) {
-               return $sql;
-       }
-
-       function strencode($s) {
-               return substr($this->addQuotes($s),1,-1);
-       }
-
-       function encodeBlob($b) {
-               return $this->strencode($b);
-       }
-
-       function decodeBlob($b) {
-               return $b;
-       }
-
-       function addQuotes($s) {
-               return $this->mConn->quote($s);
-       }
-
-       function quote_ident($s) { return $s; }
-
-       /**
-        * For now, does nothing
-        */
-       function selectDB($db) { return true; }
-
-       /**
-        * not done
-        */
-       public function setTimeout($timeout) { return; }
-
-       function ping() {
-               wfDebug("Function ping() not written for SQLite yet");
-               return true;
-       }
-
-       /**
-        * How lagged is this slave?
-        */
-       public function getLag() {
-               return 0;
-       }
-
-       /**
-        * Called by the installer script (when modified according to the MediaWikiLite installation instructions)
-        * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
-        */
-       public function setup_database() {
-               global $IP,$wgSQLiteDataDir,$wgDBTableOptions;
-               $wgDBTableOptions = '';
-               $mysql_tmpl  = "$IP/maintenance/tables.sql";
-               $mysql_iw    = "$IP/maintenance/interwiki.sql";
-               $sqlite_tmpl = "$IP/maintenance/sqlite/tables.sql";
-
-               # Make an SQLite template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
-               if (!file_exists($sqlite_tmpl)) {
-                       $sql = file_get_contents($mysql_tmpl);
-                       $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
-                       $sql = preg_replace('/^\s*(UNIQUE)?\s*(PRIMARY)?\s*KEY.+?$/m','',$sql);
-                       $sql = preg_replace('/^\s*(UNIQUE )?INDEX.+?$/m','',$sql); # These indexes should be created with a CREATE INDEX query
-                       $sql = preg_replace('/^\s*FULLTEXT.+?$/m','',$sql);        # Full text indexes
-                       $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
-                       $sql = preg_replace('/binary\(\d+\)/','BLOB',$sql);
-                       $sql = preg_replace('/(TYPE|MAX_ROWS|AVG_ROW_LENGTH)=\w+/','',$sql);
-                       $sql = preg_replace('/,\s*\)/s',')',$sql); # removing previous items may leave a trailing comma
-                       $sql = str_replace('binary','',$sql);
-                       $sql = str_replace('auto_increment','PRIMARY KEY AUTOINCREMENT',$sql);
-                       $sql = str_replace(' unsigned','',$sql);
-                       $sql = str_replace(' int ',' INTEGER ',$sql);
-                       $sql = str_replace('NOT NULL','',$sql);
-
-                       # Tidy up and write file
-                       $sql = preg_replace('/^\s*^/m','',$sql); # Remove empty lines
-                       $sql = preg_replace('/;$/m',";\n",$sql); # Separate each statement with an empty line
-                       file_put_contents($sqlite_tmpl,$sql);
-               }
-
-               # Parse the SQLite template replacing inline variables such as /*$wgDBprefix*/
-               $err = $this->sourceFile($sqlite_tmpl);
-               if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
-
-               # Use DatabasePostgres's code to populate interwiki from MySQL template
-               $f = fopen($mysql_iw,'r');
-               if ($f == false) dieout("<li>Could not find the interwiki.sql file");
-               $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
-               while (!feof($f)) {
-                       $line = fgets($f,1024);
-                       $matches = array();
-                       if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
-                       $this->query("$sql $matches[1],$matches[2])");
-               }
-       }
-
-}
-
-/**
- * @ingroup Database
- */
-class SQLiteField extends MySQLField {
-
-       function __construct() {
-       }
-
-       static function fromText($db, $table, $field) {
-               $n = new SQLiteField;
-               $n->name = $field;
-               $n->tablename = $table;
-               return $n;
-       }
-
-} // end DatabaseSqlite class
-
diff --git a/includes/DateFormatter.php b/includes/DateFormatter.php
deleted file mode 100644 (file)
index 9ef11d5..0000000
+++ /dev/null
@@ -1,283 +0,0 @@
-<?php
-
-/**
- * Date formatter, recognises dates in plain text and formats them accoding to user preferences.
- * @todo preferences, OutputPage
- * @ingroup Parser
- */
-class DateFormatter
-{
-       var $mSource, $mTarget;
-       var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD;
-
-       var $regexes, $pDays, $pMonths, $pYears;
-       var $rules, $xMonths, $preferences;
-
-       const ALL = -1;
-       const NONE = 0;
-       const MDY = 1;
-       const DMY = 2;
-       const YMD = 3;
-       const ISO1 = 4;
-       const LASTPREF = 4;
-       const ISO2 = 5;
-       const YDM = 6;
-       const DM = 7;
-       const MD = 8;
-       const LAST = 8;
-
-       /**
-        * @todo document
-        */
-       function DateFormatter() {
-               global $wgContLang;
-
-               $this->monthNames = $this->getMonthRegex();
-               for ( $i=1; $i<=12; $i++ ) {
-                       $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
-                       $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
-               }
-
-               $this->regexTrail = '(?![a-z])/iu';
-
-               # Partial regular expressions
-               $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]';
-               $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]';
-               $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]';
-               $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]';
-               $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]';
-
-               # Real regular expressions
-               $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}";
-               $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}";
-               $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}";
-               $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}";
-               $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
-               $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
-               $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
-               $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
-
-               # Extraction keys
-               # See the comments in replace() for the meaning of the letters
-               $this->keys[self::DMY] = 'jFY';
-               $this->keys[self::YDM] = 'Y jF';
-               $this->keys[self::MDY] = 'FjY';
-               $this->keys[self::YMD] = 'Y Fj';
-               $this->keys[self::DM] = 'jF';
-               $this->keys[self::MD] = 'Fj';
-               $this->keys[self::ISO1] = 'ymd'; # y means ISO year
-               $this->keys[self::ISO2] = 'ymd';
-
-               # Target date formats
-               $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
-               $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
-               $this->targets[self::MDY] = '[[F j]], [[Y]]';
-               $this->targets[self::YMD] = '[[Y]] [[F j]]';
-               $this->targets[self::DM] = '[[F j|j F]]';
-               $this->targets[self::MD] = '[[F j]]';
-               $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
-               $this->targets[self::ISO2] = '[[y-m-d]]';
-
-               # Rules
-               #            pref    source       target
-               $this->rules[self::DMY][self::MD]       = self::DM;
-               $this->rules[self::ALL][self::MD]       = self::MD;
-               $this->rules[self::MDY][self::DM]       = self::MD;
-               $this->rules[self::ALL][self::DM]       = self::DM;
-               $this->rules[self::NONE][self::ISO2]    = self::ISO1;
-
-               $this->preferences = array(
-                       'default' => self::NONE,
-                       'dmy' => self::DMY,
-                       'mdy' => self::MDY,
-                       'ymd' => self::YMD,
-                       'ISO 8601' => self::ISO1,
-               );
-       }
-
-       /**
-        * @static
-        */
-       function &getInstance() {
-               global $wgMemc;
-               static $dateFormatter = false;
-               if ( !$dateFormatter ) {
-                       $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
-                       if ( !$dateFormatter ) {
-                               $dateFormatter = new DateFormatter;
-                               $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
-                       }
-               }
-               return $dateFormatter;
-       }
-
-       /**
-        * @param string $preference User preference
-        * @param string $text Text to reformat
-        */
-       function reformat( $preference, $text ) {
-               if ( isset( $this->preferences[$preference] ) ) {
-                       $preference = $this->preferences[$preference];
-               } else {
-                       $preference = self::NONE;
-               }
-               for ( $i=1; $i<=self::LAST; $i++ ) {
-                       $this->mSource = $i;
-                       if ( isset ( $this->rules[$preference][$i] ) ) {
-                               # Specific rules
-                               $this->mTarget = $this->rules[$preference][$i];
-                       } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
-                               # General rules
-                               $this->mTarget = $this->rules[self::ALL][$i];
-                       } elseif ( $preference ) {
-                               # User preference
-                               $this->mTarget = $preference;
-                       } else {
-                               # Default
-                               $this->mTarget = $i;
-                       }
-                       $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text );
-               }
-               return $text;
-       }
-
-       /**
-        * @param $matches
-        */
-       function replace( $matches ) {
-               # Extract information from $matches
-               $bits = array();
-               $key = $this->keys[$this->mSource];
-               for ( $p=0; $p < strlen($key); $p++ ) {
-                       if ( $key{$p} != ' ' ) {
-                               $bits[$key{$p}] = $matches[$p+1];
-                       }
-               }
-
-               $format = $this->targets[$this->mTarget];
-
-               # Construct new date
-               $text = '';
-               $fail = false;
-
-               for ( $p=0; $p < strlen( $format ); $p++ ) {
-                       $char = $format{$p};
-                       switch ( $char ) {
-                               case 'd': # ISO day of month
-                                       if ( !isset($bits['d']) ) {
-                                               $text .= sprintf( '%02d', $bits['j'] );
-                                       } else {
-                                               $text .= $bits['d'];
-                                       }
-                                       break;
-                               case 'm': # ISO month
-                                       if ( !isset($bits['m']) ) {
-                                               $m = $this->makeIsoMonth( $bits['F'] );
-                                               if ( !$m || $m == '00' ) {
-                                                       $fail = true;
-                                               } else {
-                                                       $text .= $m;
-                                               }
-                                       } else {
-                                               $text .= $bits['m'];
-                                       }
-                                       break;
-                               case 'y': # ISO year
-                                       if ( !isset( $bits['y'] ) ) {
-                                               $text .= $this->makeIsoYear( $bits['Y'] );
-                                       } else {
-                                               $text .= $bits['y'];
-                                       }
-                                       break;
-                               case 'j': # ordinary day of month
-                                       if ( !isset($bits['j']) ) {
-                                               $text .= intval( $bits['d'] );
-                                       } else {
-                                               $text .= $bits['j'];
-                                       }
-                                       break;
-                               case 'F': # long month
-                                       if ( !isset( $bits['F'] ) ) {
-                                               $m = intval($bits['m']);
-                                               if ( $m > 12 || $m < 1 ) {
-                                                       $fail = true;
-                                               } else {
-                                                       global $wgContLang;
-                                                       $text .= $wgContLang->getMonthName( $m );
-                                               }
-                                       } else {
-                                               $text .= ucfirst( $bits['F'] );
-                                       }
-                                       break;
-                               case 'Y': # ordinary (optional BC) year
-                                       if ( !isset( $bits['Y'] ) ) {
-                                               $text .= $this->makeNormalYear( $bits['y'] );
-                                       } else {
-                                               $text .= $bits['Y'];
-                                       }
-                                       break;
-                               default:
-                                       $text .= $char;
-                       }
-               }
-               if ( $fail ) {
-                       $text = $matches[0];
-               }
-               return $text;
-       }
-
-       /**
-        * @todo document
-        */
-       function getMonthRegex() {
-               global $wgContLang;
-               $names = array();
-               for( $i = 1; $i <= 12; $i++ ) {
-                       $names[] = $wgContLang->getMonthName( $i );
-                       $names[] = $wgContLang->getMonthAbbreviation( $i );
-               }
-               return implode( '|', $names );
-       }
-
-       /**
-        * Makes an ISO month, e.g. 02, from a month name
-        * @param $monthName String: month name
-        * @return string ISO month name
-        */
-       function makeIsoMonth( $monthName ) {
-               global $wgContLang;
-
-               $n = $this->xMonths[$wgContLang->lc( $monthName )];
-               return sprintf( '%02d', $n );
-       }
-
-       /**
-        * @todo document
-        * @param $year String: Year name
-        * @return string ISO year name
-        */
-       function makeIsoYear( $year ) {
-               # Assumes the year is in a nice format, as enforced by the regex
-               if ( substr( $year, -2 ) == 'BC' ) {
-                       $num = intval(substr( $year, 0, -3 )) - 1;
-                       # PHP bug note: sprintf( "%04d", -1 ) fails poorly
-                       $text = sprintf( '-%04d', $num );
-
-               } else {
-                       $text = sprintf( '%04d', $year );
-               }
-               return $text;
-       }
-
-       /**
-        * @todo document
-        */
-       function makeNormalYear( $iso ) {
-               if ( $iso{0} == '-' ) {
-                       $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
-               } else {
-                       $text = intval( $iso );
-               }
-               return $text;
-       }
-}
diff --git a/includes/LBFactory.php b/includes/LBFactory.php
deleted file mode 100644 (file)
index e7b2778..0000000
+++ /dev/null
@@ -1,224 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup Database
- */
-
-/**
- * An interface for generating database load balancers
- * @ingroup Database
- */
-abstract class LBFactory {
-       static $instance;
-
-       /**
-        * Get an LBFactory instance
-        */
-       static function &singleton() {
-               if ( is_null( self::$instance ) ) {
-                       global $wgLBFactoryConf;
-                       $class = $wgLBFactoryConf['class'];
-                       self::$instance = new $class( $wgLBFactoryConf );
-               }
-               return self::$instance;
-       }
-
-       /**
-        * Destory the instance
-        * Actually used by maintenace/parserTests.inc to force to reopen connection
-        * when $wgDBprefix has changed
-        */
-       static function destroy(){
-               self::$instance = null;
-       }
-
-       /**
-        * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
-        */
-       abstract function __construct( $conf );
-
-       /**
-        * Get a load balancer object.
-        *
-        * @param string $wiki Wiki ID, or false for the current wiki
-        * @return LoadBalancer
-        */
-       abstract function getMainLB( $wiki = false );
-
-       /*
-        * Get a load balancer for external storage
-        *
-        * @param string $cluster External storage cluster, or false for core
-        * @param string $wiki Wiki ID, or false for the current wiki
-        */
-       abstract function &getExternalLB( $cluster, $wiki = false );
-
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        */
-       abstract function forEachLB( $callback, $params = array() );
-
-       /**
-        * Prepare all load balancers for shutdown
-        * STUB
-        */
-       function shutdown() {}
-
-       /**
-        * Call a method of each load balancer
-        */
-       function forEachLBCallMethod( $methodName, $args = array() ) {
-               $this->forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
-       }
-
-       /**
-        * Private helper for forEachLBCallMethod
-        */
-       function callMethod( $loadBalancer, $methodName, $args ) {
-               call_user_func_array( array( $loadBalancer, $methodName ), $args );
-       }
-
-       /**
-        * Commit changes on all master connections
-        */
-       function commitMasterChanges() {
-               $this->forEachLBCallMethod( 'commitMasterChanges' );
-       }
-}
-
-/**
- * A simple single-master LBFactory that gets its configuration from the b/c globals
- */
-class LBFactory_Simple extends LBFactory {
-       var $mainLB;
-       var $extLBs = array();
-
-       # Chronology protector
-       var $chronProt;
-
-       function __construct( $conf ) {
-               $this->chronProt = new ChronologyProtector;
-       }
-
-       function getMainLB( $wiki = false ) {
-               if ( !isset( $this->mainLB ) ) {
-                       global $wgDBservers, $wgMasterWaitTimeout;
-                       if ( !$wgDBservers ) {
-                               global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
-                               $wgDBservers = array(array(
-                                       'host' => $wgDBserver,
-                                       'user' => $wgDBuser,
-                                       'password' => $wgDBpassword,
-                                       'dbname' => $wgDBname,
-                                       'type' => $wgDBtype,
-                                       'load' => 1,
-                                       'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
-                               ));
-                       }
-
-                       $this->mainLB = new LoadBalancer( $wgDBservers, false, $wgMasterWaitTimeout, true );
-                       $this->mainLB->parentInfo( array( 'id' => 'main' ) );
-                       $this->chronProt->initLB( $this->mainLB );
-               }
-               return $this->mainLB;
-       }
-
-       function &getExternalLB( $cluster, $wiki = false ) {
-               global $wgExternalServers;
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       if ( !isset( $wgExternalServers[$cluster] ) ) {
-                               throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
-                       }
-                       $this->extLBs[$cluster] = new LoadBalancer( $wgExternalServers[$cluster] );
-                       $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
-               }
-               return $this->extLBs[$cluster];
-       }
-
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        */
-       function forEachLB( $callback, $params = array() ) {
-               if ( isset( $this->mainLB ) ) {
-                       call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
-               }
-               foreach ( $this->extLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
-               }
-       }
-
-       function shutdown() {
-               if ( $this->mainLB ) {
-                       $this->chronProt->shutdownLB( $this->mainLB );
-               }
-               $this->chronProt->shutdown();
-               $this->commitMasterChanges();
-       }
-}
-
-/**
- * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
- * Kind of like Hawking's [[Chronology Protection Agency]].
- */
-class ChronologyProtector {
-       var $startupPos;
-       var $shutdownPos = array();
-
-       /**
-        * Initialise a LoadBalancer to give it appropriate chronology protection.
-        *
-        * @param LoadBalancer $lb
-        */
-       function initLB( $lb ) {
-               if ( $this->startupPos === null ) {
-                       if ( !empty( $_SESSION[__CLASS__] ) ) {
-                               $this->startupPos = $_SESSION[__CLASS__];
-                       }
-               }
-               if ( !$this->startupPos ) {
-                       return;
-               }
-               $masterName = $lb->getServerName( 0 );
-
-               if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
-                       $info = $lb->parentInfo();
-                       $pos = $this->startupPos[$masterName];
-                       wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
-                       $lb->waitFor( $this->startupPos[$masterName] );
-               }
-       }
-
-       /**
-        * Notify the ChronologyProtector that the LoadBalancer is about to shut
-        * down. Saves replication positions.
-        *
-        * @param LoadBalancer $lb
-        */
-       function shutdownLB( $lb ) {
-               if ( session_id() != '' && $lb->getServerCount() > 1 ) {
-                       $masterName = $lb->getServerName( 0 );
-                       if ( !isset( $this->shutdownPos[$masterName] ) ) {
-                               $pos = $lb->getMasterPos();
-                               $info = $lb->parentInfo();
-                               wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
-                               $this->shutdownPos[$masterName] = $pos;
-                       }
-               }
-       }
-
-       /**
-        * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
-        * May commit chronology data to persistent storage.
-        */
-       function shutdown() {
-               if ( session_id() != '' && count( $this->shutdownPos ) ) {
-                       wfDebug( __METHOD__.": saving master pos for " .
-                               count( $this->shutdownPos ) . " master(s)\n" );
-                       $_SESSION[__CLASS__] = $this->shutdownPos;
-               }
-       }
-}
diff --git a/includes/LBFactory_Multi.php b/includes/LBFactory_Multi.php
deleted file mode 100644 (file)
index b5fc1f6..0000000
+++ /dev/null
@@ -1,224 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup Database
- */
-
-
-/**
- * A multi-wiki, multi-master factory for Wikimedia and similar installations.
- * Ignores the old configuration globals
- *
- * Configuration:
- *     sectionsByDB                A map of database names to section names
- *
- *     sectionLoads                A 2-d map. For each section, gives a map of server names to load ratios.
- *                                 For example: array( 'section1' => array( 'db1' => 100, 'db2' => 100 ) )
- *
- *     serverTemplate              A server info associative array as documented for $wgDBservers. The host,
- *                                 hostName and load entries will be overridden.
- *
- *     groupLoadsBySection         A 3-d map giving server load ratios for each section and group. For example:
- *                                 array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
- *
- *     groupLoadsByDB              A 3-d map giving server load ratios by DB name.
- *
- *     hostsByName                 A map of hostname to IP address.
- *
- *     externalLoads               A map of external storage cluster name to server load map
- *
- *     externalTemplateOverrides   A set of server info keys overriding serverTemplate for external storage
- *
- *     templateOverridesByServer   A 2-d map overriding serverTemplate and externalTemplateOverrides on a
- *                                 server-by-server basis. Applies to both core and external storage.
- *
- *     templateOverridesByCluster  A 2-d map overriding the server info by external storage cluster
- *
- *     masterTemplateOverrides     An override array for all master servers.
- *
- * @ingroup Database
- */
-class LBFactory_Multi extends LBFactory {
-       // Required settings
-       var $sectionsByDB, $sectionLoads, $serverTemplate;
-       // Optional settings
-       var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
-       var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
-       var $templateOverridesByCluster, $masterTemplateOverrides;
-       // Other stuff
-       var $conf, $mainLBs = array(), $extLBs = array();
-       var $localSection = null;
-
-       function __construct( $conf ) {
-               $this->chronProt = new ChronologyProtector;
-               $this->conf = $conf;
-               $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
-               $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
-                       'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
-                       'templateOverridesByCluster', 'masterTemplateOverrides' );
-
-               foreach ( $required as $key ) {
-                       if ( !isset( $conf[$key] ) ) {
-                               throw new MWException( __CLASS__.": $key is required in configuration" );
-                       }
-                       $this->$key = $conf[$key];
-               }
-
-               foreach ( $optional as $key ) {
-                       if ( isset( $conf[$key] ) ) {
-                               $this->$key = $conf[$key];
-                       }
-               }
-       }
-
-       function getSectionForWiki( $wiki ) {
-               list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
-               if ( isset( $this->sectionsByDB[$dbName] ) ) {
-                       return $this->sectionsByDB[$dbName];
-               } else {
-                       return 'DEFAULT';
-               }
-       }
-
-       function getMainLB( $wiki = false ) {
-               // Determine section
-               if ( $wiki === false ) {
-                       if ( $this->localSection === null ) {
-                               $this->localSection = $this->getSectionForWiki( $wiki );
-                       }
-                       $section = $this->localSection;
-               } else {
-                       $section = $this->getSectionForWiki( $wiki );
-               }
-
-               if ( !isset( $this->mainLBs[$section] ) ) {
-                       list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
-                       $groupLoads = array();
-                       if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
-                               $groupLoads = $this->groupLoadsByDB[$dbName];
-                       }
-                       if ( isset( $this->groupLoadsBySection[$section] ) ) {
-                               $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
-                       }
-                       $this->mainLBs[$section] = $this->newLoadBalancer( $this->serverTemplate,
-                               $this->sectionLoads[$section], $groupLoads, "main-$section" );
-                       $this->chronProt->initLB( $this->mainLBs[$section] );
-               }
-               return $this->mainLBs[$section];
-       }
-
-       function &getExternalLB( $cluster, $wiki = false ) {
-               if ( !isset( $this->extLBs[$cluster] ) ) {
-                       if ( !isset( $this->externalLoads[$cluster] ) ) {
-                               throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
-                       }
-                       $template = $this->serverTemplate;
-                       if ( isset( $this->externalTemplateOverrides ) ) {
-                               $template = $this->externalTemplateOverrides + $template;
-                       }
-                       if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
-                               $template = $this->templateOverridesByCluster[$cluster] + $template;
-                       }
-                       $this->extLBs[$cluster] = $this->newLoadBalancer( $template,
-                               $this->externalLoads[$cluster], array(), "ext-$cluster" );
-               }
-               return $this->extLBs[$cluster];
-       }
-
-       /**
-        * Make a new load balancer object based on template and load array
-        */
-       function newLoadBalancer( $template, $loads, $groupLoads, $id ) {
-               global $wgMasterWaitTimeout;
-               $servers = $this->makeServerArray( $template, $loads, $groupLoads );
-               $lb = new LoadBalancer( $servers, false, $wgMasterWaitTimeout );
-               $lb->parentInfo( array( 'id' => $id ) );
-               return $lb;
-       }
-
-       /**
-        * Make a server array as expected by LoadBalancer::__construct, using a template and load array
-        */
-       function makeServerArray( $template, $loads, $groupLoads ) {
-               $servers = array();
-               $master = true;
-               $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
-               foreach ( $groupLoadsByServer as $server => $stuff ) {
-                       if ( !isset( $loads[$server] ) ) {
-                               $loads[$server] = 0;
-                       }
-               }
-               foreach ( $loads as $serverName => $load ) {
-                       $serverInfo = $template;
-                       if ( $master ) {
-                               $serverInfo['master'] = true;
-                               if ( isset( $this->masterTemplateOverrides ) ) {
-                                       $serverInfo = $this->masterTemplateOverrides + $serverInfo;
-                               }
-                               $master = false;
-                       }
-                       if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
-                               $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
-                       }
-                       if ( isset( $groupLoadsByServer[$serverName] ) ) {
-                               $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
-                       }
-                       if ( isset( $this->hostsByName[$serverName] ) ) {
-                               $serverInfo['host'] = $this->hostsByName[$serverName];
-                       } else {
-                               $serverInfo['host'] = $serverName;
-                       }
-                       $serverInfo['hostName'] = $serverName;
-                       $serverInfo['load'] = $load;
-                       $servers[] = $serverInfo;
-               }
-               return $servers;
-       }
-
-       /**
-        * Take a group load array indexed by group then server, and reindex it by server then group
-        */
-       function reindexGroupLoads( $groupLoads ) {
-               $reindexed = array();
-               foreach ( $groupLoads as $group => $loads ) {
-                       foreach ( $loads as $server => $load ) {
-                               $reindexed[$server][$group] = $load;
-                       }
-               }
-               return $reindexed;
-       }
-
-       /**
-        * Get the database name and prefix based on the wiki ID
-        */
-       function getDBNameAndPrefix( $wiki = false ) {
-               if ( $wiki === false ) {
-                       global $wgDBname, $wgDBprefix;
-                       return array( $wgDBname, $wgDBprefix );
-               } else {
-                       return wfSplitWikiID( $wiki );
-               }
-       }
-
-       /**
-        * Execute a function for each tracked load balancer
-        * The callback is called with the load balancer as the first parameter,
-        * and $params passed as the subsequent parameters.
-        */
-       function forEachLB( $callback, $params = array() ) {
-               foreach ( $this->mainLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
-               }
-               foreach ( $this->extLBs as $lb ) {
-                       call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
-               }
-       }
-
-       function shutdown() {
-               foreach ( $this->mainLBs as $lb ) {
-                       $this->chronProt->shutdownLB( $lb );
-               }
-               $this->chronProt->shutdown();
-               $this->commitMasterChanges();
-       }
-}
diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php
deleted file mode 100644 (file)
index 4280713..0000000
+++ /dev/null
@@ -1,894 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup Database
- */
-
-/**
- * Database load balancing object
- *
- * @todo document
- * @ingroup Database
- */
-class LoadBalancer {
-       /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
-       /* private */ var $mFailFunction, $mErrorConnection;
-       /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
-       /* private */ var $mWaitForPos, $mWaitTimeout;
-       /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
-       /* private */ var $mParentInfo, $mLagTimes;
-
-       function __construct( $servers, $failFunction = false, $waitTimeout = 10, $unused = false )
-       {
-               $this->mServers = $servers;
-               $this->mFailFunction = $failFunction;
-               $this->mReadIndex = -1;
-               $this->mWriteIndex = -1;
-               $this->mConns = array(
-                       'local' => array(),
-                       'foreignUsed' => array(),
-                       'foreignFree' => array() );
-               $this->mLastIndex = -1;
-               $this->mLoads = array();
-               $this->mWaitForPos = false;
-               $this->mWaitTimeout = $waitTimeout;
-               $this->mLaggedSlaveMode = false;
-               $this->mErrorConnection = false;
-               $this->mAllowLag = false;
-
-               foreach( $servers as $i => $server ) {
-                       $this->mLoads[$i] = $server['load'];
-                       if ( isset( $server['groupLoads'] ) ) {
-                               foreach ( $server['groupLoads'] as $group => $ratio ) {
-                                       if ( !isset( $this->mGroupLoads[$group] ) ) {
-                                               $this->mGroupLoads[$group] = array();
-                                       }
-                                       $this->mGroupLoads[$group][$i] = $ratio;
-                               }
-                       }
-               }
-       }
-
-       static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
-       {
-               return new LoadBalancer( $servers, $failFunction, $waitTimeout );
-       }
-
-       /**
-        * Get or set arbitrary data used by the parent object, usually an LBFactory
-        */
-       function parentInfo( $x = null ) {
-               return wfSetVar( $this->mParentInfo, $x );
-       }
-
-       /**
-        * Given an array of non-normalised probabilities, this function will select
-        * an element and return the appropriate key
-        */
-       function pickRandom( $weights )
-       {
-               if ( !is_array( $weights ) || count( $weights ) == 0 ) {
-                       return false;
-               }
-
-               $sum = array_sum( $weights );
-               if ( $sum == 0 ) {
-                       # No loads on any of them
-                       # In previous versions, this triggered an unweighted random selection,
-                       # but this feature has been removed as of April 2006 to allow for strict
-                       # separation of query groups.
-                       return false;
-               }
-               $max = mt_getrandmax();
-               $rand = mt_rand(0, $max) / $max * $sum;
-
-               $sum = 0;
-               foreach ( $weights as $i => $w ) {
-                       $sum += $w;
-                       if ( $sum >= $rand ) {
-                               break;
-                       }
-               }
-               return $i;
-       }
-
-       function getRandomNonLagged( $loads, $wiki = false ) {
-               # Unset excessively lagged servers
-               $lags = $this->getLagTimes( $wiki );
-               foreach ( $lags as $i => $lag ) {
-                       if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
-                               if ( $lag === false ) {
-                                       wfDebug( "Server #$i is not replicating\n" );
-                                       unset( $loads[$i] );
-                               } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
-                                       wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
-                                       unset( $loads[$i] );
-                               }
-                       }
-               }
-
-               # Find out if all the slaves with non-zero load are lagged
-               $sum = 0;
-               foreach ( $loads as $load ) {
-                       $sum += $load;
-               }
-               if ( $sum == 0 ) {
-                       # No appropriate DB servers except maybe the master and some slaves with zero load
-                       # Do NOT use the master
-                       # Instead, this function will return false, triggering read-only mode,
-                       # and a lagged slave will be used instead.
-                       return false;
-               }
-
-               if ( count( $loads ) == 0 ) {
-                       return false;
-               }
-
-               #wfDebugLog( 'connect', var_export( $loads, true ) );
-
-               # Return a random representative of the remainder
-               return $this->pickRandom( $loads );
-       }
-
-       /**
-        * Get the index of the reader connection, which may be a slave
-        * This takes into account load ratios and lag times. It should
-        * always return a consistent index during a given invocation
-        *
-        * Side effect: opens connections to databases
-        */
-       function getReaderIndex( $group = false, $wiki = false ) {
-               global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
-
-               # FIXME: For now, only go through all this for mysql databases
-               if ($wgDBtype != 'mysql') {
-                       return $this->getWriterIndex();
-               }
-
-               if ( count( $this->mServers ) == 1 )  {
-                       # Skip the load balancing if there's only one server
-                       return 0;
-               } elseif ( $group === false and $this->mReadIndex >= 0 ) {
-                       # Shortcut if generic reader exists already
-                       return $this->mReadIndex;
-               }
-
-               wfProfileIn( __METHOD__ );
-
-               $totalElapsed = 0;
-
-               # convert from seconds to microseconds
-               $timeout = $wgDBClusterTimeout * 1e6;
-
-               # Find the relevant load array
-               if ( $group !== false ) {
-                       if ( isset( $this->mGroupLoads[$group] ) ) {
-                               $nonErrorLoads = $this->mGroupLoads[$group];
-                       } else {
-                               # No loads for this group, return false and the caller can use some other group
-                               wfDebug( __METHOD__.": no loads for group $group\n" );
-                               wfProfileOut( __METHOD__ );
-                               return false;
-                       }
-               } else {
-                       $nonErrorLoads = $this->mLoads;
-               }
-
-               if ( !$nonErrorLoads ) {
-                       throw new MWException( "Empty server array given to LoadBalancer" );
-               }
-
-               $i = false;
-               $found = false;
-               $laggedSlaveMode = false;
-
-               # First try quickly looking through the available servers for a server that
-               # meets our criteria
-               do {
-                       $totalThreadsConnected = 0;
-                       $overloadedServers = 0;
-                       $currentLoads = $nonErrorLoads;
-                       while ( count( $currentLoads ) ) {
-                               if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
-                                       $i = $this->pickRandom( $currentLoads );
-                               } else {
-                                       $i = $this->getRandomNonLagged( $currentLoads, $wiki );
-                                       if ( $i === false && count( $currentLoads ) != 0 )  {
-                                               # All slaves lagged. Switch to read-only mode
-                                               $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
-                                               $i = $this->pickRandom( $currentLoads );
-                                               $laggedSlaveMode = true;
-                                       }
-                               }
-
-                               if ( $i === false ) {
-                                       # pickRandom() returned false
-                                       # This is permanent and means the configuration wants us to return false
-                                       wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
-                                       wfProfileOut( __METHOD__ );
-                                       return false;
-                               }
-
-                               wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
-                               $conn = $this->openConnection( $i, $wiki );
-
-                               if ( !$conn ) {
-                                       wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
-                                       unset( $nonErrorLoads[$i] );
-                                       unset( $currentLoads[$i] );
-                                       continue;
-                               }
-
-                               if ( isset( $this->mServers[$i]['max threads'] ) ) {
-                                       $status = $conn->getStatus("Thread%");
-                                       if ( $wiki !== false ) {
-                                               $this->reuseConnection( $conn );
-                                       }
-                                       if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
-                                               $totalThreadsConnected += $status['Threads_connected'];
-                                               $overloadedServers++;
-                                               unset( $currentLoads[$i] );
-                                       } else {
-                                               # Max threads satisfied, return this server
-                                               break 2;
-                                       }
-                               } else {
-                                       # No maximum, return this server
-                                       if ( $wiki !== false ) {
-                                               $this->reuseConnection( $conn );
-                                       }
-                                       $found = true;
-                                       break 2;
-                               }
-                       }
-
-                       # No server found yet
-                       $i = false;
-
-                       # If all servers were down, quit now
-                       if ( !count( $nonErrorLoads ) ) {
-                               wfDebugLog( 'connect', "All servers down\n" );
-                               break;
-                       }
-
-                       # Some servers must have been overloaded
-                       if ( $overloadedServers == 0 ) {
-                               throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
-                       }
-                       # Back off for a while
-                       # Scale the sleep time by the number of connected threads, to produce a
-                       # roughly constant global poll rate
-                       $avgThreads = $totalThreadsConnected / $overloadedServers;
-                       $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
-               } while ( $totalElapsed < $timeout );
-
-               if ( $totalElapsed >= $timeout ) {
-                       wfDebugLog( 'connect', "All servers busy\n" );
-                       $this->mErrorConnection = false;
-                       $this->mLastError = 'All servers busy';
-               }
-
-               if ( $i !== false ) {
-                       # Wait for the session master pos for a short time
-                       if ( $this->mWaitForPos && $i > 0 ) {
-                               if ( !$this->doWait( $i ) ) {
-                                       $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
-                               }
-                       }
-                       if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
-                               $this->mReadIndex = $i;
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-               return $i;
-       }
-
-       /**
-        * Wait for a specified number of microseconds, and return the period waited
-        */
-       function sleep( $t ) {
-               wfProfileIn( __METHOD__ );
-               wfDebug( __METHOD__.": waiting $t us\n" );
-               usleep( $t );
-               wfProfileOut( __METHOD__ );
-               return $t;
-       }
-
-       /**
-        * Get a random server to use in a query group
-        * @deprecated use getReaderIndex
-        */
-       function getGroupIndex( $group ) {
-               return $this->getReaderIndex( $group );
-       }
-
-       /**
-        * Set the master wait position
-        * If a DB_SLAVE connection has been opened already, waits
-        * Otherwise sets a variable telling it to wait if such a connection is opened
-        */
-       public function waitFor( $pos ) {
-               wfProfileIn( __METHOD__ );
-               $this->mWaitForPos = $pos;
-               $i = $this->mReadIndex;
-
-               if ( $i > 0 ) {
-                       if ( !$this->doWait( $i ) ) {
-                               $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
-                               $this->mLaggedSlaveMode = true;
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Get any open connection to a given server index, local or foreign
-        * Returns false if there is no connection open
-        */
-       function getAnyOpenConnection( $i ) {
-               foreach ( $this->mConns as $type => $conns ) {
-                       if ( !empty( $conns[$i] ) ) {
-                               return reset( $conns[$i] );
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Wait for a given slave to catch up to the master pos stored in $this
-        */
-       function doWait( $index ) {
-               # Find a connection to wait on
-               $conn = $this->getAnyOpenConnection( $index );
-               if ( !$conn ) {
-                       wfDebug( __METHOD__ . ": no connection open\n" );
-                       return false;
-               }
-
-               wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
-               $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
-
-               if ( $result == -1 || is_null( $result ) ) {
-                       # Timed out waiting for slave, use master instead
-                       wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
-                       return false;
-               } else {
-                       wfDebug( __METHOD__.": Done\n" );
-                       return true;
-               }
-       }
-
-       /**
-        * Get a connection by index
-        * This is the main entry point for this class.
-        */
-       public function &getConnection( $i, $groups = array(), $wiki = false ) {
-               global $wgDBtype;
-               wfProfileIn( __METHOD__ );
-
-               if ( $wiki === wfWikiID() ) {
-                       $wiki = false;
-               }
-
-               # Query groups
-               if ( $i == DB_MASTER ) {
-                       $i = $this->getWriterIndex();
-               } elseif ( !is_array( $groups ) ) {
-                       $groupIndex = $this->getReaderIndex( $groups, $wiki );
-                       if ( $groupIndex !== false ) {
-                               $serverName = $this->getServerName( $groupIndex );
-                               wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
-                               $i = $groupIndex;
-                       }
-               } else {
-                       foreach ( $groups as $group ) {
-                               $groupIndex = $this->getReaderIndex( $group, $wiki );
-                               if ( $groupIndex !== false ) {
-                                       $serverName = $this->getServerName( $groupIndex );
-                                       wfDebug( __METHOD__.": using server $serverName for group $group\n" );
-                                       $i = $groupIndex;
-                                       break;
-                               }
-                       }
-               }
-
-               # Operation-based index
-               if ( $i == DB_SLAVE ) {
-                       $i = $this->getReaderIndex( false, $wiki );
-               } elseif ( $i == DB_LAST ) {
-                       # Just use $this->mLastIndex, which should already be set
-                       $i = $this->mLastIndex;
-                       if ( $i === -1 ) {
-                               # Oh dear, not set, best to use the writer for safety
-                               wfDebug( "Warning: DB_LAST used when there was no previous index\n" );
-                               $i = $this->getWriterIndex();
-                       }
-               }
-               # Couldn't find a working server in getReaderIndex()?
-               if ( $i === false ) {
-                       $this->reportConnectionError( $this->mErrorConnection );
-               }
-
-               # Now we have an explicit index into the servers array
-               $conn = $this->openConnection( $i, $wiki );
-               if ( !$conn ) {
-                       $this->reportConnectionError( $this->mErrorConnection );
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $conn;
-       }
-
-       /**
-        * Mark a foreign connection as being available for reuse under a different
-        * DB name or prefix. This mechanism is reference-counted, and must be called
-        * the same number of times as getConnection() to work.
-        */
-       public function reuseConnection( $conn ) {
-               $serverIndex = $conn->getLBInfo('serverIndex');
-               $refCount = $conn->getLBInfo('foreignPoolRefCount');
-               $dbName = $conn->getDBname();
-               $prefix = $conn->tablePrefix();
-               if ( strval( $prefix ) !== '' ) {
-                       $wiki = "$dbName-$prefix";
-               } else {
-                       $wiki = $dbName;
-               }
-               if ( $serverIndex === null || $refCount === null ) {
-                       wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
-                       /**
-                        * This can happen in code like:
-                        *   foreach ( $dbs as $db ) {
-                        *     $conn = $lb->getConnection( DB_SLAVE, array(), $db );
-                        *     ...
-                        *     $lb->reuseConnection( $conn );
-                        *   }
-                        * When a connection to the local DB is opened in this way, reuseConnection()
-                        * should be ignored
-                        */
-                       return;
-               }
-               if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
-                       throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
-               }
-               $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
-               if ( $refCount <= 0 ) {
-                       $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
-                       unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
-                       wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
-               } else {
-                       wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
-               }
-       }
-
-       /**
-        * Open a connection to the server given by the specified index
-        * Index must be an actual index into the array.
-        * If the server is already open, returns it.
-        *
-        * On error, returns false, and the connection which caused the
-        * error will be available via $this->mErrorConnection.
-        *
-        * @param integer $i Server index
-        * @param string $wiki Wiki ID to open
-        * @return Database
-        *
-        * @access private
-        */
-       function openConnection( $i, $wiki = false ) {
-               wfProfileIn( __METHOD__ );
-               if ( $wiki !== false ) {
-                       $conn = $this->openForeignConnection( $i, $wiki );
-                       wfProfileOut( __METHOD__);
-                       return $conn;
-               }
-               if ( isset( $this->mConns['local'][$i][0] ) ) {
-                       $conn = $this->mConns['local'][$i][0];
-               } else {
-                       $server = $this->mServers[$i];
-                       $server['serverIndex'] = $i;
-                       $conn = $this->reallyOpenConnection( $server );
-                       if ( $conn->isOpen() ) {
-                               $this->mConns['local'][$i][0] = $conn;
-                       } else {
-                               wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
-                               $this->mErrorConnection = $conn;
-                               $conn = false;
-                       }
-               }
-               $this->mLastIndex = $i;
-               wfProfileOut( __METHOD__ );
-               return $conn;
-       }
-
-       /**
-        * Open a connection to a foreign DB, or return one if it is already open.
-        *
-        * Increments a reference count on the returned connection which locks the
-        * connection to the requested wiki. This reference count can be
-        * decremented by calling reuseConnection().
-        *
-        * If a connection is open to the appropriate server already, but with the wrong
-        * database, it will be switched to the right database and returned, as long as
-        * it has been freed first with reuseConnection().
-        *
-        * On error, returns false, and the connection which caused the
-        * error will be available via $this->mErrorConnection.
-        *
-        * @param integer $i Server index
-        * @param string $wiki Wiki ID to open
-        * @return Database
-        */
-       function openForeignConnection( $i, $wiki ) {
-               wfProfileIn(__METHOD__);
-               list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
-               if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
-                       // Reuse an already-used connection
-                       $conn = $this->mConns['foreignUsed'][$i][$wiki];
-                       wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
-               } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
-                       // Reuse a free connection for the same wiki
-                       $conn = $this->mConns['foreignFree'][$i][$wiki];
-                       unset( $this->mConns['foreignFree'][$i][$wiki] );
-                       $this->mConns['foreignUsed'][$i][$wiki] = $conn;
-                       wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
-               } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
-                       // Reuse a connection from another wiki
-                       $conn = reset( $this->mConns['foreignFree'][$i] );
-                       $oldWiki = key( $this->mConns['foreignFree'][$i] );
-
-                       if ( !$conn->selectDB( $dbName ) ) {
-                               global $wguname;
-                               $this->mLastError = "Error selecting database $dbName on server " .
-                                       $conn->getServer() . " from client host {$wguname['nodename']}\n";
-                               $this->mErrorConnection = $conn;
-                               $conn = false;
-                       } else {
-                               $conn->tablePrefix( $prefix );
-                               unset( $this->mConns['foreignFree'][$i][$oldWiki] );
-                               $this->mConns['foreignUsed'][$i][$wiki] = $conn;
-                               wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
-                       }
-               } else {
-                       // Open a new connection
-                       $server = $this->mServers[$i];
-                       $server['serverIndex'] = $i;
-                       $server['foreignPoolRefCount'] = 0;
-                       $conn = $this->reallyOpenConnection( $server, $dbName );
-                       if ( !$conn->isOpen() ) {
-                               wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
-                               $this->mErrorConnection = $conn;
-                               $conn = false;
-                       } else {
-                               $this->mConns['foreignUsed'][$i][$wiki] = $conn;
-                               wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
-                       }
-               }
-
-               // Increment reference count
-               if ( $conn ) {
-                       $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
-                       $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
-               }
-               wfProfileOut(__METHOD__);
-               return $conn;
-       }
-
-       /**
-        * Test if the specified index represents an open connection
-        * @access private
-        */
-       function isOpen( $index ) {
-               if( !is_integer( $index ) ) {
-                       return false;
-               }
-               return (bool)$this->getAnyOpenConnection( $index );
-       }
-
-       /**
-        * Really opens a connection. Uncached.
-        * Returns a Database object whether or not the connection was successful.
-        * @access private
-        */
-       function reallyOpenConnection( $server, $dbNameOverride = false ) {
-               if( !is_array( $server ) ) {
-                       throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
-               }
-
-               extract( $server );
-               if ( $dbNameOverride !== false ) {
-                       $dbname = $dbNameOverride;
-               }
-
-               # Get class for this database type
-               $class = 'Database' . ucfirst( $type );
-
-               # Create object
-               wfDebug( "Connecting to $host $dbname...\n" );
-               $db = new $class( $host, $user, $password, $dbname, 1, $flags );
-               if ( $db->isOpen() ) {
-                       wfDebug( "Connected\n" );
-               } else {
-                       wfDebug( "Failed\n" );
-               }
-               $db->setLBInfo( $server );
-               if ( isset( $server['fakeSlaveLag'] ) ) {
-                       $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
-               }
-               if ( isset( $server['fakeMaster'] ) ) {
-                       $db->setFakeMaster( true );
-               }
-               return $db;
-       }
-
-       function reportConnectionError( &$conn ) {
-               wfProfileIn( __METHOD__ );
-               # Prevent infinite recursion
-
-               static $reporting = false;
-               if ( !$reporting ) {
-                       $reporting = true;
-                       if ( !is_object( $conn ) ) {
-                               // No last connection, probably due to all servers being too busy
-                               $conn = new Database;
-                               if ( $this->mFailFunction ) {
-                                       $conn->failFunction( $this->mFailFunction );
-                                       $conn->reportConnectionError( $this->mLastError );
-                               } else {
-                                       // If all servers were busy, mLastError will contain something sensible
-                                       throw new DBConnectionError( $conn, $this->mLastError );
-                               }
-                       } else {
-                               if ( $this->mFailFunction ) {
-                                       $conn->failFunction( $this->mFailFunction );
-                               } else {
-                                       $conn->failFunction( false );
-                               }
-                               $server = $conn->getProperty( 'mServer' );
-                               $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
-                       }
-                       $reporting = false;
-               }
-               wfProfileOut( __METHOD__ );
-       }
-
-       function getWriterIndex() {
-               return 0;
-       }
-
-       /**
-        * Returns true if the specified index is a valid server index
-        */
-       function haveIndex( $i ) {
-               return array_key_exists( $i, $this->mServers );
-       }
-
-       /**
-        * Returns true if the specified index is valid and has non-zero load
-        */
-       function isNonZeroLoad( $i ) {
-               return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
-       }
-
-       /**
-        * Get the number of defined servers (not the number of open connections)
-        */
-       function getServerCount() {
-               return count( $this->mServers );
-       }
-
-       /**
-        * Get the host name or IP address of the server with the specified index
-        */
-       function getServerName( $i ) {
-               if ( isset( $this->mServers[$i]['hostName'] ) ) {
-                       return $this->mServers[$i]['hostName'];
-               } elseif ( isset( $this->mServers[$i]['host'] ) ) {
-                       return $this->mServers[$i]['host'];
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * Get the current master position for chronology control purposes
-        * @return mixed
-        */
-       function getMasterPos() {
-               # If this entire request was served from a slave without opening a connection to the
-               # master (however unlikely that may be), then we can fetch the position from the slave.
-               $masterConn = $this->getAnyOpenConnection( 0 );
-               if ( !$masterConn ) {
-                       for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
-                               $conn = $this->getAnyOpenConnection( $i );
-                               if ( $conn ) {
-                                       wfDebug( "Master pos fetched from slave\n" );
-                                       return $conn->getSlavePos();
-                               }
-                       }
-               } else {
-                       wfDebug( "Master pos fetched from master\n" );
-                       return $masterConn->getMasterPos();
-               }
-               return false;
-       }
-
-       /**
-        * Close all open connections
-        */
-       function closeAll() {
-               foreach ( $this->mConns as $conns2 ) {
-                       foreach  ( $conns2 as $conns3 ) {
-                               foreach ( $conns3 as $conn ) {
-                                       $conn->close();
-                               }
-                       }
-               }
-               $this->mConns = array(
-                       'local' => array(),
-                       'foreignFree' => array(),
-                       'foreignUsed' => array(),
-               );
-       }
-
-       /**
-        * Close a connection
-        * Using this function makes sure the LoadBalancer knows the connection is closed.
-        * If you use $conn->close() directly, the load balancer won't update its state.
-        */
-       function closeConnecton( $conn ) {
-               $done = false;
-               foreach ( $this->mConns as $i1 => $conns2 ) {
-                       foreach ( $conns2 as $i2 => $conns3 ) {
-                               foreach ( $conns3 as $i3 => $candidateConn ) {
-                                       if ( $conn === $candidateConn ) {
-                                               $conn->close();
-                                               unset( $this->mConns[$i1][$i2][$i3] );
-                                               $done = true;
-                                               break;
-                                       }
-                               }
-                       }
-               }
-               if ( !$done ) {
-                       $conn->close();
-               }
-       }
-
-       /**
-        * Commit transactions on all open connections
-        */
-       function commitAll() {
-               foreach ( $this->mConns as $conns2 ) {
-                       foreach ( $conns2 as $conns3 ) {
-                               foreach ( $conns3 as $conn ) {
-                                       $conn->immediateCommit();
-                               }
-                       }
-               }
-       }
-
-       /* Issue COMMIT only on master, only if queries were done on connection */
-       function commitMasterChanges() {
-               // Always 0, but who knows.. :)
-               $masterIndex = $this->getWriterIndex();
-               foreach ( $this->mConns as $type => $conns2 ) {
-                       if ( empty( $conns2[$masterIndex] ) ) {
-                               continue;
-                       }
-                       foreach ( $conns2[$masterIndex] as $conn ) {
-                               if ( $conn->lastQuery() != '' ) {
-                                       $conn->commit();
-                               }
-                       }
-               }
-       }
-
-       function waitTimeout( $value = NULL ) {
-               return wfSetVar( $this->mWaitTimeout, $value );
-       }
-
-       function getLaggedSlaveMode() {
-               return $this->mLaggedSlaveMode;
-       }
-
-       /* Disables/enables lag checks */
-       function allowLagged($mode=null) {
-               if ($mode===null)
-                       return $this->mAllowLagged;
-               $this->mAllowLagged=$mode;
-       }
-
-       function pingAll() {
-               $success = true;
-               foreach ( $this->mConns as $conns2 ) {
-                       foreach ( $conns2 as $conns3 ) {
-                               foreach ( $conns3 as $conn ) {
-                                       if ( !$conn->ping() ) {
-                                               $success = false;
-                                       }
-                               }
-                       }
-               }
-               return $success;
-       }
-
-       /**
-        * Get the hostname and lag time of the most-lagged slave.
-        * This is useful for maintenance scripts that need to throttle their updates.
-        * May attempt to open connections to slaves on the default DB.
-        */
-       function getMaxLag() {
-               $maxLag = -1;
-               $host = '';
-               foreach ( $this->mServers as $i => $conn ) {
-                       $conn = $this->getAnyOpenConnection( $i );
-                       if ( !$conn ) {
-                               $conn = $this->openConnection( $i );
-                       }
-                       if ( !$conn ) {
-                               continue;
-                       }
-                       $lag = $conn->getLag();
-                       if ( $lag > $maxLag ) {
-                               $maxLag = $lag;
-                               $host = $this->mServers[$i]['host'];
-                       }
-               }
-               return array( $host, $maxLag );
-       }
-
-       /**
-        * Get lag time for each server
-        * Results are cached for a short time in memcached, and indefinitely in the process cache
-        */
-       function getLagTimes( $wiki = false ) {
-               wfProfileIn( __METHOD__ );
-
-               if ( !isset( $this->mLagTimes ) ) {
-                       $expiry = 5;
-                       $requestRate = 10;
-
-                       global $wgMemc;
-                       $masterName = $this->getServerName( 0 );
-                       $memcKey = wfMemcKey( 'lag_times', $masterName );
-                       $times = $wgMemc->get( $memcKey );
-                       if ( $times ) {
-                               # Randomly recache with probability rising over $expiry
-                               $elapsed = time() - $times['timestamp'];
-                               $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
-                               if ( mt_rand( 0, $chance ) != 0 ) {
-                                       unset( $times['timestamp'] );
-                                       wfProfileOut( __METHOD__ );
-                                       return $times;
-                               }
-                               wfIncrStats( 'lag_cache_miss_expired' );
-                       } else {
-                               wfIncrStats( 'lag_cache_miss_absent' );
-                       }
-
-                       # Cache key missing or expired
-
-                       $times = array();
-                       foreach ( $this->mServers as $i => $conn ) {
-                               if ($i == 0) { # Master
-                                       $times[$i] = 0;
-                               } elseif ( false !== ( $conn = $this->getAnyOpenConnection( $i ) ) ) {
-                                       $times[$i] = $conn->getLag();
-                               } elseif ( false !== ( $conn = $this->openConnection( $i, $wiki ) ) ) {
-                                       $times[$i] = $conn->getLag();
-                               }
-                       }
-
-                       # Add a timestamp key so we know when it was cached
-                       $times['timestamp'] = time();
-                       $wgMemc->set( $memcKey, $times, $expiry );
-
-                       # But don't give the timestamp to the caller
-                       unset($times['timestamp']);
-                       $this->mLagTimes = $times;
-               }
-               wfProfileOut( __METHOD__ );
-               return $this->mLagTimes;
-       }
-}
diff --git a/includes/Parser.php b/includes/Parser.php
deleted file mode 100644 (file)
index 4daa8f2..0000000
+++ /dev/null
@@ -1,4974 +0,0 @@
-<?php
-/**
- * @defgroup Parser Parser
- *
- * @file
- * @ingroup Parser
- * File for Parser and related classes
- */
-
-
-/**
- * PHP Parser - Processes wiki markup (which uses a more user-friendly
- * syntax, such as "[[link]]" for making links), and provides a one-way
- * transformation of that wiki markup it into XHTML output / markup
- * (which in turn the browser understands, and can display).
- *
- * <pre>
- * There are five main entry points into the Parser class:
- * parse()
- *   produces HTML output
- * preSaveTransform().
- *   produces altered wiki markup.
- * preprocess()
- *   removes HTML comments and expands templates
- * cleanSig()
- *   Cleans a signature before saving it to preferences
- * extractSections()
- *   Extracts sections from an article for section editing
- *
- * Globals used:
- *    objects:   $wgLang, $wgContLang
- *
- * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
- *
- * settings:
- *  $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
- *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
- *  $wgLocaltimezone, $wgAllowSpecialInclusion*,
- *  $wgMaxArticleSize*
- *
- *  * only within ParserOptions
- * </pre>
- *
- * @ingroup Parser
- */
-class Parser
-{
-       /**
-        * Update this version number when the ParserOutput format
-        * changes in an incompatible way, so the parser cache
-        * can automatically discard old data.
-        */
-       const VERSION = '1.6.4';
-
-       # Flags for Parser::setFunctionHook
-       # Also available as global constants from Defines.php
-       const SFH_NO_HASH = 1;
-       const SFH_OBJECT_ARGS = 2;
-
-       # Constants needed for external link processing
-       # Everything except bracket, space, or control characters
-       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
-       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
-               \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
-
-       // State constants for the definition list colon extraction
-       const COLON_STATE_TEXT = 0;
-       const COLON_STATE_TAG = 1;
-       const COLON_STATE_TAGSTART = 2;
-       const COLON_STATE_CLOSETAG = 3;
-       const COLON_STATE_TAGSLASH = 4;
-       const COLON_STATE_COMMENT = 5;
-       const COLON_STATE_COMMENTDASH = 6;
-       const COLON_STATE_COMMENTDASHDASH = 7;
-
-       // Flags for preprocessToDom
-       const PTD_FOR_INCLUSION = 1;
-
-       // Allowed values for $this->mOutputType
-       // Parameter to startExternalParse().
-       const OT_HTML = 1;
-       const OT_WIKI = 2;
-       const OT_PREPROCESS = 3;
-       const OT_MSG = 3;
-
-       // Marker Suffix needs to be accessible staticly.
-       const MARKER_SUFFIX = "-QINU\x7f";
-
-       /**#@+
-        * @private
-        */
-       # Persistent:
-       var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
-               $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
-               $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
-
-
-       # Cleared with clearState():
-       var $mOutput, $mAutonumber, $mDTopen, $mStripState;
-       var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
-       var $mInterwikiLinkHolders, $mLinkHolders;
-       var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
-       var $mTplExpandCache; // empty-frame expansion cache
-       var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
-       var $mExpensiveFunctionCount; // number of expensive parser function calls
-
-       # Temporary
-       # These are variables reset at least once per parse regardless of $clearState
-       var $mOptions,      // ParserOptions object
-               $mTitle,        // Title context, used for self-link rendering and similar things
-               $mOutputType,   // Output type, one of the OT_xxx constants
-               $ot,            // Shortcut alias, see setOutputType()
-               $mRevisionId,   // ID to display in {{REVISIONID}} tags
-               $mRevisionTimestamp, // The timestamp of the specified revision ID
-               $mRevIdForTs;   // The revision ID which was used to fetch the timestamp
-
-       /**#@-*/
-
-       /**
-        * Constructor
-        *
-        * @public
-        */
-       function __construct( $conf = array() ) {
-               $this->mConf = $conf;
-               $this->mTagHooks = array();
-               $this->mTransparentTagHooks = array();
-               $this->mFunctionHooks = array();
-               $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
-               $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
-               $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
-                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
-               $this->mVarCache = array();
-               if ( isset( $conf['preprocessorClass'] ) ) {
-                       $this->mPreprocessorClass = $conf['preprocessorClass'];
-               } else {
-                       $this->mPreprocessorClass = 'Preprocessor_Hash';
-               }
-               $this->mMarkerIndex = 0;
-               $this->mFirstCall = true;
-       }
-
-       /**
-        * Do various kinds of initialisation on the first call of the parser
-        */
-       function firstCallInit() {
-               if ( !$this->mFirstCall ) {
-                       return;
-               }
-               $this->mFirstCall = false;
-
-               wfProfileIn( __METHOD__ );
-
-               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
-               CoreParserFunctions::register( $this );
-               $this->initialiseVariables();
-
-               wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Clear Parser state
-        *
-        * @private
-        */
-       function clearState() {
-               wfProfileIn( __METHOD__ );
-               if ( $this->mFirstCall ) {
-                       $this->firstCallInit();
-               }
-               $this->mOutput = new ParserOutput;
-               $this->mAutonumber = 0;
-               $this->mLastSection = '';
-               $this->mDTopen = false;
-               $this->mIncludeCount = array();
-               $this->mStripState = new StripState;
-               $this->mArgStack = false;
-               $this->mInPre = false;
-               $this->mInterwikiLinkHolders = array(
-                       'texts' => array(),
-                       'titles' => array()
-               );
-               $this->mLinkHolders = array(
-                       'namespaces' => array(),
-                       'dbkeys' => array(),
-                       'queries' => array(),
-                       'texts' => array(),
-                       'titles' => array()
-               );
-               $this->mRevisionTimestamp = $this->mRevisionId = null;
-
-               /**
-                * Prefix for temporary replacement strings for the multipass parser.
-                * \x07 should never appear in input as it's disallowed in XML.
-                * Using it at the front also gives us a little extra robustness
-                * since it shouldn't match when butted up against identifier-like
-                * string constructs.
-                *
-                * Must not consist of all title characters, or else it will change
-                * the behaviour of <nowiki> in a link.
-                */
-               #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
-               # Changed to \x7f to allow XML double-parsing -- TS
-               $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
-
-
-               # Clear these on every parse, bug 4549
-               $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
-
-               $this->mShowToc = true;
-               $this->mForceTocPosition = false;
-               $this->mIncludeSizes = array(
-                       'post-expand' => 0,
-                       'arg' => 0,
-               );
-               $this->mPPNodeCount = 0;
-               $this->mDefaultSort = false;
-               $this->mHeadings = array();
-               $this->mDoubleUnderscores = array();
-               $this->mExpensiveFunctionCount = 0;
-
-               # Fix cloning
-               if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
-                       $this->mPreprocessor = null;
-               }
-
-               wfRunHooks( 'ParserClearState', array( &$this ) );
-               wfProfileOut( __METHOD__ );
-       }
-
-       function setOutputType( $ot ) {
-               $this->mOutputType = $ot;
-               // Shortcut alias
-               $this->ot = array(
-                       'html' => $ot == self::OT_HTML,
-                       'wiki' => $ot == self::OT_WIKI,
-                       'pre' => $ot == self::OT_PREPROCESS,
-               );
-       }
-
-       /**
-        * Set the context title
-        */
-       function setTitle( $t ) {
-               if ( !$t || $t instanceof FakeTitle ) {
-                       $t = Title::newFromText( 'NO TITLE' );
-               }
-               if ( strval( $t->getFragment() ) !== '' ) {
-                       # Strip the fragment to avoid various odd effects
-                       $this->mTitle = clone $t;
-                       $this->mTitle->setFragment( '' );
-               } else {
-                       $this->mTitle = $t;
-               }
-       }
-
-       /**
-        * Accessor for mUniqPrefix.
-        *
-        * @public
-        */
-       function uniqPrefix() {
-               if( !isset( $this->mUniqPrefix ) ) {
-                       // @fixme this is probably *horribly wrong*
-                       // LanguageConverter seems to want $wgParser's uniqPrefix, however
-                       // if this is called for a parser cache hit, the parser may not
-                       // have ever been initialized in the first place.
-                       // Not really sure what the heck is supposed to be going on here.
-                       return '';
-                       //throw new MWException( "Accessing uninitialized mUniqPrefix" );
-               }
-               return $this->mUniqPrefix;
-       }
-
-       /**
-        * Convert wikitext to HTML
-        * Do not call this function recursively.
-        *
-        * @param string $text Text we want to parse
-        * @param Title &$title A title object
-        * @param array $options
-        * @param boolean $linestart
-        * @param boolean $clearState
-        * @param int $revid number to pass in {{REVISIONID}}
-        * @return ParserOutput a ParserOutput
-        */
-       public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
-               /**
-                * First pass--just handle <nowiki> sections, pass the rest off
-                * to internalParse() which does all the real work.
-                */
-
-               global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
-               $fname = 'Parser::parse-' . wfGetCaller();
-               wfProfileIn( __METHOD__ );
-               wfProfileIn( $fname );
-
-               if ( $clearState ) {
-                       $this->clearState();
-               }
-
-               $this->mOptions = $options;
-               $this->setTitle( $title );
-               $oldRevisionId = $this->mRevisionId;
-               $oldRevisionTimestamp = $this->mRevisionTimestamp;
-               if( $revid !== null ) {
-                       $this->mRevisionId = $revid;
-                       $this->mRevisionTimestamp = null;
-               }
-               $this->setOutputType( self::OT_HTML );
-               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-               # No more strip!
-               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->internalParse( $text );
-               $text = $this->mStripState->unstripGeneral( $text );
-
-               # Clean up special characters, only run once, next-to-last before doBlockLevels
-               $fixtags = array(
-                       # french spaces, last one Guillemet-left
-                       # only if there is something before the space
-                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
-                       # french spaces, Guillemet-right
-                       '/(\\302\\253) /' => '\\1&nbsp;',
-                       '/&nbsp;(!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
-               );
-               $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
-
-               # only once and last
-               $text = $this->doBlockLevels( $text, $linestart );
-
-               $this->replaceLinkHolders( $text );
-
-               # the position of the parserConvert() call should not be changed. it
-               # assumes that the links are all replaced and the only thing left
-               # is the <nowiki> mark.
-               # Side-effects: this calls $this->mOutput->setTitleText()
-               $text = $wgContLang->parserConvert( $text, $this );
-
-               $text = $this->mStripState->unstripNoWiki( $text );
-
-               wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
-
-//!JF Move to its own function
-
-               $uniq_prefix = $this->mUniqPrefix;
-               $matches = array();
-               $elements = array_keys( $this->mTransparentTagHooks );
-               $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-
-               foreach( $matches as $marker => $data ) {
-                       list( $element, $content, $params, $tag ) = $data;
-                       $tagName = strtolower( $element );
-                       if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
-                               $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
-                                       array( $content, $params, $this ) );
-                       } else {
-                               $output = $tag;
-                       }
-                       $this->mStripState->general->setPair( $marker, $output );
-               }
-               $text = $this->mStripState->unstripGeneral( $text );
-
-               $text = Sanitizer::normalizeCharReferences( $text );
-
-               if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
-                       $text = Parser::tidy($text);
-               } else {
-                       # attempt to sanitize at least some nesting problems
-                       # (bug #2702 and quite a few others)
-                       $tidyregs = array(
-                               # ''Something [http://www.cool.com cool''] -->
-                               # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
-                               '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
-                               '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
-                               # fix up an anchor inside another anchor, only
-                               # at least for a single single nested link (bug 3695)
-                               '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
-                               '\\1\\2</a>\\3</a>\\1\\4</a>',
-                               # fix div inside inline elements- doBlockLevels won't wrap a line which
-                               # contains a div, so fix it up here; replace
-                               # div with escaped text
-                               '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
-                               '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
-                               # remove empty italic or bold tag pairs, some
-                               # introduced by rules above
-                               '/<([bi])><\/\\1>/' => '',
-                       );
-
-                       $text = preg_replace(
-                               array_keys( $tidyregs ),
-                               array_values( $tidyregs ),
-                               $text );
-               }
-               global $wgExpensiveParserFunctionLimit;
-               if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
-                       $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
-               }
-
-               wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
-
-               # Information on include size limits, for the benefit of users who try to skirt them
-               if ( $this->mOptions->getEnableLimitReport() ) {
-                       global $wgExpensiveParserFunctionLimit;
-                       $max = $this->mOptions->getMaxIncludeSize();
-                       $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
-                       $limitReport =
-                               "NewPP limit report\n" .
-                               "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
-                               "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
-                               "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
-                               $PFreport;
-                       wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
-                       $text .= "\n<!-- \n$limitReport-->\n";
-               }
-               $this->mOutput->setText( $text );
-               $this->mRevisionId = $oldRevisionId;
-               $this->mRevisionTimestamp = $oldRevisionTimestamp;
-               wfProfileOut( $fname );
-               wfProfileOut( __METHOD__ );
-
-               return $this->mOutput;
-       }
-
-       /**
-        * Recursive parser entry point that can be called from an extension tag
-        * hook.
-        */
-       function recursiveTagParse( $text ) {
-               wfProfileIn( __METHOD__ );
-               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->internalParse( $text );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       /**
-        * Expand templates and variables in the text, producing valid, static wikitext.
-        * Also removes comments.
-        */
-       function preprocess( $text, $title, $options, $revid = null ) {
-               wfProfileIn( __METHOD__ );
-               $this->clearState();
-               $this->setOutputType( self::OT_PREPROCESS );
-               $this->mOptions = $options;
-               $this->setTitle( $title );
-               if( $revid !== null ) {
-                       $this->mRevisionId = $revid;
-               }
-               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->replaceVariables( $text );
-               $text = $this->mStripState->unstripBoth( $text );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       /**
-        * Get a random string
-        *
-        * @private
-        * @static
-        */
-       function getRandomString() {
-               return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
-       }
-
-       function &getTitle() { return $this->mTitle; }
-       function getOptions() { return $this->mOptions; }
-       function getRevisionId() { return $this->mRevisionId; }
-
-       function getFunctionLang() {
-               global $wgLang, $wgContLang;
-
-               $target = $this->mOptions->getTargetLanguage();
-               if ( $target !== null ) {
-                       return $target;
-               } else {
-                       return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
-               }
-       }
-
-       /**
-        * Get a preprocessor object
-        */
-       function getPreprocessor() {
-               if ( !isset( $this->mPreprocessor ) ) {
-                       $class = $this->mPreprocessorClass;
-                       $this->mPreprocessor = new $class( $this );
-               }
-               return $this->mPreprocessor;
-       }
-
-       /**
-        * Replaces all occurrences of HTML-style comments and the given tags
-        * in the text with a random marker and returns the next text. The output
-        * parameter $matches will be an associative array filled with data in
-        * the form:
-        *   'UNIQ-xxxxx' => array(
-        *     'element',
-        *     'tag content',
-        *     array( 'param' => 'x' ),
-        *     '<element param="x">tag content</element>' ) )
-        *
-        * @param $elements list of element names. Comments are always extracted.
-        * @param $text Source text string.
-        * @param $uniq_prefix
-        *
-        * @public
-        * @static
-        */
-       function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
-               static $n = 1;
-               $stripped = '';
-               $matches = array();
-
-               $taglist = implode( '|', $elements );
-               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
-
-               while ( '' != $text ) {
-                       $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
-                       $stripped .= $p[0];
-                       if( count( $p ) < 5 ) {
-                               break;
-                       }
-                       if( count( $p ) > 5 ) {
-                               // comment
-                               $element    = $p[4];
-                               $attributes = '';
-                               $close      = '';
-                               $inside     = $p[5];
-                       } else {
-                               // tag
-                               $element    = $p[1];
-                               $attributes = $p[2];
-                               $close      = $p[3];
-                               $inside     = $p[4];
-                       }
-
-                       $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
-                       $stripped .= $marker;
-
-                       if ( $close === '/>' ) {
-                               // Empty element tag, <tag />
-                               $content = null;
-                               $text = $inside;
-                               $tail = null;
-                       } else {
-                               if( $element == '!--' ) {
-                                       $end = '/(-->)/';
-                               } else {
-                                       $end = "/(<\\/$element\\s*>)/i";
-                               }
-                               $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
-                               $content = $q[0];
-                               if( count( $q ) < 3 ) {
-                                       # No end tag -- let it run out to the end of the text.
-                                       $tail = '';
-                                       $text = '';
-                               } else {
-                                       $tail = $q[1];
-                                       $text = $q[2];
-                               }
-                       }
-
-                       $matches[$marker] = array( $element,
-                               $content,
-                               Sanitizer::decodeTagAttributes( $attributes ),
-                               "<$element$attributes$close$content$tail" );
-               }
-               return $stripped;
-       }
-
-       /**
-        * Get a list of strippable XML-like elements
-        */
-       function getStripList() {
-               global $wgRawHtml;
-               $elements = $this->mStripList;
-               if( $wgRawHtml ) {
-                       $elements[] = 'html';
-               }
-               if( $this->mOptions->getUseTeX() ) {
-                       $elements[] = 'math';
-               }
-               return $elements;
-       }
-
-       /**
-        * @deprecated use replaceVariables
-        */
-       function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
-               return $text;
-       }
-
-       /**
-        * Restores pre, math, and other extensions removed by strip()
-        *
-        * always call unstripNoWiki() after this one
-        * @private
-        * @deprecated use $this->mStripState->unstrip()
-        */
-       function unstrip( $text, $state ) {
-               return $state->unstripGeneral( $text );
-       }
-
-       /**
-        * Always call this after unstrip() to preserve the order
-        *
-        * @private
-        * @deprecated use $this->mStripState->unstrip()
-        */
-       function unstripNoWiki( $text, $state ) {
-               return $state->unstripNoWiki( $text );
-       }
-
-       /**
-        * @deprecated use $this->mStripState->unstripBoth()
-        */
-       function unstripForHTML( $text ) {
-               return $this->mStripState->unstripBoth( $text );
-       }
-
-       /**
-        * Add an item to the strip state
-        * Returns the unique tag which must be inserted into the stripped text
-        * The tag will be replaced with the original text in unstrip()
-        *
-        * @private
-        */
-       function insertStripItem( $text ) {
-               $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
-               $this->mMarkerIndex++;
-               $this->mStripState->general->setPair( $rnd, $text );
-               return $rnd;
-       }
-
-       /**
-        * Interface with html tidy, used if $wgUseTidy = true.
-        * If tidy isn't able to correct the markup, the original will be
-        * returned in all its glory with a warning comment appended.
-        *
-        * Either the external tidy program or the in-process tidy extension
-        * will be used depending on availability. Override the default
-        * $wgTidyInternal setting to disable the internal if it's not working.
-        *
-        * @param string $text Hideous HTML input
-        * @return string Corrected HTML output
-        * @public
-        * @static
-        */
-       function tidy( $text ) {
-               global $wgTidyInternal;
-               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
-' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
-'<head><title>test</title></head><body>'.$text.'</body></html>';
-               if( $wgTidyInternal ) {
-                       $correctedtext = Parser::internalTidy( $wrappedtext );
-               } else {
-                       $correctedtext = Parser::externalTidy( $wrappedtext );
-               }
-               if( is_null( $correctedtext ) ) {
-                       wfDebug( "Tidy error detected!\n" );
-                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
-               }
-               return $correctedtext;
-       }
-
-       /**
-        * Spawn an external HTML tidy process and get corrected markup back from it.
-        *
-        * @private
-        * @static
-        */
-       function externalTidy( $text ) {
-               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
-               $fname = 'Parser::externalTidy';
-               wfProfileIn( $fname );
-
-               $cleansource = '';
-               $opts = ' -utf8';
-
-               $descriptorspec = array(
-                       0 => array('pipe', 'r'),
-                       1 => array('pipe', 'w'),
-                       2 => array('file', wfGetNull(), 'a')
-               );
-               $pipes = array();
-               $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
-               if (is_resource($process)) {
-                       // Theoretically, this style of communication could cause a deadlock
-                       // here. If the stdout buffer fills up, then writes to stdin could
-                       // block. This doesn't appear to happen with tidy, because tidy only
-                       // writes to stdout after it's finished reading from stdin. Search
-                       // for tidyParseStdin and tidySaveStdout in console/tidy.c
-                       fwrite($pipes[0], $text);
-                       fclose($pipes[0]);
-                       while (!feof($pipes[1])) {
-                               $cleansource .= fgets($pipes[1], 1024);
-                       }
-                       fclose($pipes[1]);
-                       proc_close($process);
-               }
-
-               wfProfileOut( $fname );
-
-               if( $cleansource == '' && $text != '') {
-                       // Some kind of error happened, so we couldn't get the corrected text.
-                       // Just give up; we'll use the source text and append a warning.
-                       return null;
-               } else {
-                       return $cleansource;
-               }
-       }
-
-       /**
-        * Use the HTML tidy PECL extension to use the tidy library in-process,
-        * saving the overhead of spawning a new process.
-        *
-        * 'pear install tidy' should be able to compile the extension module.
-        *
-        * @private
-        * @static
-        */
-       function internalTidy( $text ) {
-               global $wgTidyConf, $IP, $wgDebugTidy;
-               $fname = 'Parser::internalTidy';
-               wfProfileIn( $fname );
-
-               $tidy = new tidy;
-               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
-               $tidy->cleanRepair();
-               if( $tidy->getStatus() == 2 ) {
-                       // 2 is magic number for fatal error
-                       // http://www.php.net/manual/en/function.tidy-get-status.php
-                       $cleansource = null;
-               } else {
-                       $cleansource = tidy_get_output( $tidy );
-               }
-               if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
-                       $cleansource .= "<!--\nTidy reports:\n" .
-                               str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
-                               "\n-->";
-               }
-
-               wfProfileOut( $fname );
-               return $cleansource;
-       }
-
-       /**
-        * parse the wiki syntax used to render tables
-        *
-        * @private
-        */
-       function doTableStuff ( $text ) {
-               $fname = 'Parser::doTableStuff';
-               wfProfileIn( $fname );
-
-               $lines = explode ( "\n" , $text );
-               $td_history = array (); // Is currently a td tag open?
-               $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
-               $tr_history = array (); // Is currently a tr tag open?
-               $tr_attributes = array (); // history of tr attributes
-               $has_opened_tr = array(); // Did this table open a <tr> element?
-               $indent_level = 0; // indent level of the table
-               foreach ( $lines as $key => $line )
-               {
-                       $line = trim ( $line );
-
-                       if( $line == '' ) { // empty line, go to next line
-                               continue;
-                       }
-                       $first_character = $line{0};
-                       $matches = array();
-
-                       if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
-                               // First check if we are starting a new table
-                               $indent_level = strlen( $matches[1] );
-
-                               $attributes = $this->mStripState->unstripBoth( $matches[2] );
-                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
-
-                               $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
-                               array_push ( $td_history , false );
-                               array_push ( $last_tag_history , '' );
-                               array_push ( $tr_history , false );
-                               array_push ( $tr_attributes , '' );
-                               array_push ( $has_opened_tr , false );
-                       } else if ( count ( $td_history ) == 0 ) {
-                               // Don't do any of the following
-                               continue;
-                       } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
-                               // We are ending a table
-                               $line = '</table>' . substr ( $line , 2 );
-                               $last_tag = array_pop ( $last_tag_history );
-
-                               if ( !array_pop ( $has_opened_tr ) ) {
-                                       $line = "<tr><td></td></tr>{$line}";
-                               }
-
-                               if ( array_pop ( $tr_history ) ) {
-                                       $line = "</tr>{$line}";
-                               }
-
-                               if ( array_pop ( $td_history ) ) {
-                                       $line = "</{$last_tag}>{$line}";
-                               }
-                               array_pop ( $tr_attributes );
-                               $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
-                       } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
-                               // Now we have a table row
-                               $line = preg_replace( '#^\|-+#', '', $line );
-
-                               // Whats after the tag is now only attributes
-                               $attributes = $this->mStripState->unstripBoth( $line );
-                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
-                               array_pop ( $tr_attributes );
-                               array_push ( $tr_attributes , $attributes );
-
-                               $line = '';
-                               $last_tag = array_pop ( $last_tag_history );
-                               array_pop ( $has_opened_tr );
-                               array_push ( $has_opened_tr , true );
-
-                               if ( array_pop ( $tr_history ) ) {
-                                       $line = '</tr>';
-                               }
-
-                               if ( array_pop ( $td_history ) ) {
-                                       $line = "</{$last_tag}>{$line}";
-                               }
-
-                               $lines[$key] = $line;
-                               array_push ( $tr_history , false );
-                               array_push ( $td_history , false );
-                               array_push ( $last_tag_history , '' );
-                       }
-                       else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 )  == '|+' ) {
-                               // This might be cell elements, td, th or captions
-                               if ( substr ( $line , 0 , 2 ) == '|+' ) {
-                                       $first_character = '+';
-                                       $line = substr ( $line , 1 );
-                               }
-
-                               $line = substr ( $line , 1 );
-
-                               if ( $first_character == '!' ) {
-                                       $line = str_replace ( '!!' , '||' , $line );
-                               }
-
-                               // Split up multiple cells on the same line.
-                               // FIXME : This can result in improper nesting of tags processed
-                               // by earlier parser steps, but should avoid splitting up eg
-                               // attribute values containing literal "||".
-                               $cells = StringUtils::explodeMarkup( '||' , $line );
-
-                               $lines[$key] = '';
-
-                               // Loop through each table cell
-                               foreach ( $cells as $cell )
-                               {
-                                       $previous = '';
-                                       if ( $first_character != '+' )
-                                       {
-                                               $tr_after = array_pop ( $tr_attributes );
-                                               if ( !array_pop ( $tr_history ) ) {
-                                                       $previous = "<tr{$tr_after}>\n";
-                                               }
-                                               array_push ( $tr_history , true );
-                                               array_push ( $tr_attributes , '' );
-                                               array_pop ( $has_opened_tr );
-                                               array_push ( $has_opened_tr , true );
-                                       }
-
-                                       $last_tag = array_pop ( $last_tag_history );
-
-                                       if ( array_pop ( $td_history ) ) {
-                                               $previous = "</{$last_tag}>{$previous}";
-                                       }
-
-                                       if ( $first_character == '|' ) {
-                                               $last_tag = 'td';
-                                       } else if ( $first_character == '!' ) {
-                                               $last_tag = 'th';
-                                       } else if ( $first_character == '+' ) {
-                                               $last_tag = 'caption';
-                                       } else {
-                                               $last_tag = '';
-                                       }
-
-                                       array_push ( $last_tag_history , $last_tag );
-
-                                       // A cell could contain both parameters and data
-                                       $cell_data = explode ( '|' , $cell , 2 );
-
-                                       // Bug 553: Note that a '|' inside an invalid link should not
-                                       // be mistaken as delimiting cell parameters
-                                       if ( strpos( $cell_data[0], '[[' ) !== false ) {
-                                               $cell = "{$previous}<{$last_tag}>{$cell}";
-                                       } else if ( count ( $cell_data ) == 1 )
-                                               $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
-                                       else {
-                                               $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
-                                               $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
-                                               $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
-                                       }
-
-                                       $lines[$key] .= $cell;
-                                       array_push ( $td_history , true );
-                               }
-                       }
-               }
-
-               // Closing open td, tr && table
-               while ( count ( $td_history ) > 0 )
-               {
-                       if ( array_pop ( $td_history ) ) {
-                               $lines[] = '</td>' ;
-                       }
-                       if ( array_pop ( $tr_history ) ) {
-                               $lines[] = '</tr>' ;
-                       }
-                       if ( !array_pop ( $has_opened_tr ) ) {
-                               $lines[] = "<tr><td></td></tr>" ;
-                       }
-
-                       $lines[] = '</table>' ;
-               }
-
-               $output = implode ( "\n" , $lines ) ;
-
-               // special case: don't return empty table
-               if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
-                       $output = '';
-               }
-
-               wfProfileOut( $fname );
-
-               return $output;
-       }
-
-       /**
-        * Helper function for parse() that transforms wiki markup into
-        * HTML. Only called for $mOutputType == self::OT_HTML.
-        *
-        * @private
-        */
-       function internalParse( $text ) {
-               $isMain = true;
-               $fname = 'Parser::internalParse';
-               wfProfileIn( $fname );
-
-               # Hook to suspend the parser in this state
-               if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
-                       wfProfileOut( $fname );
-                       return $text ;
-               }
-
-               $text = $this->replaceVariables( $text );
-               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
-               wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
-
-               // Tables need to come after variable replacement for things to work
-               // properly; putting them before other transformations should keep
-               // exciting things like link expansions from showing up in surprising
-               // places.
-               $text = $this->doTableStuff( $text );
-
-               $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
-
-               $text = $this->doDoubleUnderscore( $text );
-               $text = $this->doHeadings( $text );
-               if($this->mOptions->getUseDynamicDates()) {
-                       $df = DateFormatter::getInstance();
-                       $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
-               }
-               $text = $this->doAllQuotes( $text );
-               $text = $this->replaceInternalLinks( $text );
-               $text = $this->replaceExternalLinks( $text );
-
-               # replaceInternalLinks may sometimes leave behind
-               # absolute URLs, which have to be masked to hide them from replaceExternalLinks
-               $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
-
-               $text = $this->doMagicLinks( $text );
-               $text = $this->formatHeadings( $text, $isMain );
-
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * Replace special strings like "ISBN xxx" and "RFC xxx" with
-        * magic external links.
-        *
-        * @private
-        */
-       function doMagicLinks( $text ) {
-               wfProfileIn( __METHOD__ );
-               $text = preg_replace_callback(
-                       '!(?:                           # Start cases
-                           <a.*?</a> |                 # Skip link text
-                           <.*?> |                     # Skip stuff inside HTML elements
-                           (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
-                           ISBN\s+(\b                  # ISBN, capture number as m[2]
-                                     (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
-                                     (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
-                                     [0-9Xx]                 # check digit
-                                   \b)
-                       )!x', array( &$this, 'magicLinkCallback' ), $text );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       function magicLinkCallback( $m ) {
-               if ( substr( $m[0], 0, 1 ) == '<' ) {
-                       # Skip HTML element
-                       return $m[0];
-               } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
-                       $isbn = $m[2];
-                       $num = strtr( $isbn, array(
-                               '-' => '',
-                               ' ' => '',
-                               'x' => 'X',
-                       ));
-                       $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
-                       $text = '<a href="' .
-                               $titleObj->escapeLocalUrl() .
-                               "\" class=\"internal\">ISBN $isbn</a>";
-               } else {
-                       if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
-                               $keyword = 'RFC';
-                               $urlmsg = 'rfcurl';
-                               $id = $m[1];
-                       } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
-                               $keyword = 'PMID';
-                               $urlmsg = 'pubmedurl';
-                               $id = $m[1];
-                       } else {
-                               throw new MWException( __METHOD__.': unrecognised match type "' .
-                                       substr($m[0], 0, 20 ) . '"' );
-                       }
-
-                       $url = wfMsg( $urlmsg, $id);
-                       $sk = $this->mOptions->getSkin();
-                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
-                       $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
-               }
-               return $text;
-       }
-
-       /**
-        * Parse headers and return html
-        *
-        * @private
-        */
-       function doHeadings( $text ) {
-               $fname = 'Parser::doHeadings';
-               wfProfileIn( $fname );
-               for ( $i = 6; $i >= 1; --$i ) {
-                       $h = str_repeat( '=', $i );
-                       $text = preg_replace( "/^$h(.+)$h\\s*$/m",
-                         "<h$i>\\1</h$i>", $text );
-               }
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * Replace single quotes with HTML markup
-        * @private
-        * @return string the altered text
-        */
-       function doAllQuotes( $text ) {
-               $fname = 'Parser::doAllQuotes';
-               wfProfileIn( $fname );
-               $outtext = '';
-               $lines = explode( "\n", $text );
-               foreach ( $lines as $line ) {
-                       $outtext .= $this->doQuotes ( $line ) . "\n";
-               }
-               $outtext = substr($outtext, 0,-1);
-               wfProfileOut( $fname );
-               return $outtext;
-       }
-
-       /**
-        * Helper function for doAllQuotes()
-        */
-       public function doQuotes( $text ) {
-               $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-               if ( count( $arr ) == 1 )
-                       return $text;
-               else
-               {
-                       # First, do some preliminary work. This may shift some apostrophes from
-                       # being mark-up to being text. It also counts the number of occurrences
-                       # of bold and italics mark-ups.
-                       $i = 0;
-                       $numbold = 0;
-                       $numitalics = 0;
-                       foreach ( $arr as $r )
-                       {
-                               if ( ( $i % 2 ) == 1 )
-                               {
-                                       # If there are ever four apostrophes, assume the first is supposed to
-                                       # be text, and the remaining three constitute mark-up for bold text.
-                                       if ( strlen( $arr[$i] ) == 4 )
-                                       {
-                                               $arr[$i-1] .= "'";
-                                               $arr[$i] = "'''";
-                                       }
-                                       # If there are more than 5 apostrophes in a row, assume they're all
-                                       # text except for the last 5.
-                                       else if ( strlen( $arr[$i] ) > 5 )
-                                       {
-                                               $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
-                                               $arr[$i] = "'''''";
-                                       }
-                                       # Count the number of occurrences of bold and italics mark-ups.
-                                       # We are not counting sequences of five apostrophes.
-                                       if ( strlen( $arr[$i] ) == 2 )      { $numitalics++;             }
-                                       else if ( strlen( $arr[$i] ) == 3 ) { $numbold++;                }
-                                       else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
-                               }
-                               $i++;
-                       }
-
-                       # If there is an odd number of both bold and italics, it is likely
-                       # that one of the bold ones was meant to be an apostrophe followed
-                       # by italics. Which one we cannot know for certain, but it is more
-                       # likely to be one that has a single-letter word before it.
-                       if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
-                       {
-                               $i = 0;
-                               $firstsingleletterword = -1;
-                               $firstmultiletterword = -1;
-                               $firstspace = -1;
-                               foreach ( $arr as $r )
-                               {
-                                       if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
-                                       {
-                                               $x1 = substr ($arr[$i-1], -1);
-                                               $x2 = substr ($arr[$i-1], -2, 1);
-                                               if ($x1 == ' ') {
-                                                       if ($firstspace == -1) $firstspace = $i;
-                                               } else if ($x2 == ' ') {
-                                                       if ($firstsingleletterword == -1) $firstsingleletterword = $i;
-                                               } else {
-                                                       if ($firstmultiletterword == -1) $firstmultiletterword = $i;
-                                               }
-                                       }
-                                       $i++;
-                               }
-
-                               # If there is a single-letter word, use it!
-                               if ($firstsingleletterword > -1)
-                               {
-                                       $arr [ $firstsingleletterword ] = "''";
-                                       $arr [ $firstsingleletterword-1 ] .= "'";
-                               }
-                               # If not, but there's a multi-letter word, use that one.
-                               else if ($firstmultiletterword > -1)
-                               {
-                                       $arr [ $firstmultiletterword ] = "''";
-                                       $arr [ $firstmultiletterword-1 ] .= "'";
-                               }
-                               # ... otherwise use the first one that has neither.
-                               # (notice that it is possible for all three to be -1 if, for example,
-                               # there is only one pentuple-apostrophe in the line)
-                               else if ($firstspace > -1)
-                               {
-                                       $arr [ $firstspace ] = "''";
-                                       $arr [ $firstspace-1 ] .= "'";
-                               }
-                       }
-
-                       # Now let's actually convert our apostrophic mush to HTML!
-                       $output = '';
-                       $buffer = '';
-                       $state = '';
-                       $i = 0;
-                       foreach ($arr as $r)
-                       {
-                               if (($i % 2) == 0)
-                               {
-                                       if ($state == 'both')
-                                               $buffer .= $r;
-                                       else
-                                               $output .= $r;
-                               }
-                               else
-                               {
-                                       if (strlen ($r) == 2)
-                                       {
-                                               if ($state == 'i')
-                                               { $output .= '</i>'; $state = ''; }
-                                               else if ($state == 'bi')
-                                               { $output .= '</i>'; $state = 'b'; }
-                                               else if ($state == 'ib')
-                                               { $output .= '</b></i><b>'; $state = 'b'; }
-                                               else if ($state == 'both')
-                                               { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
-                                               else # $state can be 'b' or ''
-                                               { $output .= '<i>'; $state .= 'i'; }
-                                       }
-                                       else if (strlen ($r) == 3)
-                                       {
-                                               if ($state == 'b')
-                                               { $output .= '</b>'; $state = ''; }
-                                               else if ($state == 'bi')
-                                               { $output .= '</i></b><i>'; $state = 'i'; }
-                                               else if ($state == 'ib')
-                                               { $output .= '</b>'; $state = 'i'; }
-                                               else if ($state == 'both')
-                                               { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
-                                               else # $state can be 'i' or ''
-                                               { $output .= '<b>'; $state .= 'b'; }
-                                       }
-                                       else if (strlen ($r) == 5)
-                                       {
-                                               if ($state == 'b')
-                                               { $output .= '</b><i>'; $state = 'i'; }
-                                               else if ($state == 'i')
-                                               { $output .= '</i><b>'; $state = 'b'; }
-                                               else if ($state == 'bi')
-                                               { $output .= '</i></b>'; $state = ''; }
-                                               else if ($state == 'ib')
-                                               { $output .= '</b></i>'; $state = ''; }
-                                               else if ($state == 'both')
-                                               { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
-                                               else # ($state == '')
-                                               { $buffer = ''; $state = 'both'; }
-                                       }
-                               }
-                               $i++;
-                       }
-                       # Now close all remaining tags.  Notice that the order is important.
-                       if ($state == 'b' || $state == 'ib')
-                               $output .= '</b>';
-                       if ($state == 'i' || $state == 'bi' || $state == 'ib')
-                               $output .= '</i>';
-                       if ($state == 'bi')
-                               $output .= '</b>';
-                       # There might be lonely ''''', so make sure we have a buffer
-                       if ($state == 'both' && $buffer)
-                               $output .= '<b><i>'.$buffer.'</i></b>';
-                       return $output;
-               }
-       }
-
-       /**
-        * Replace external links
-        *
-        * Note: this is all very hackish and the order of execution matters a lot.
-        * Make sure to run maintenance/parserTests.php if you change this code.
-        *
-        * @private
-        */
-       function replaceExternalLinks( $text ) {
-               global $wgContLang;
-               $fname = 'Parser::replaceExternalLinks';
-               wfProfileIn( $fname );
-
-               $sk = $this->mOptions->getSkin();
-
-               $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-
-               $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
-
-               $i = 0;
-               while ( $i<count( $bits ) ) {
-                       $url = $bits[$i++];
-                       $protocol = $bits[$i++];
-                       $text = $bits[$i++];
-                       $trail = $bits[$i++];
-
-                       # The characters '<' and '>' (which were escaped by
-                       # removeHTMLtags()) should not be included in
-                       # URLs, per RFC 2396.
-                       $m2 = array();
-                       if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                               $text = substr($url, $m2[0][1]) . ' ' . $text;
-                               $url = substr($url, 0, $m2[0][1]);
-                       }
-
-                       # If the link text is an image URL, replace it with an <img> tag
-                       # This happened by accident in the original parser, but some people used it extensively
-                       $img = $this->maybeMakeExternalImage( $text );
-                       if ( $img !== false ) {
-                               $text = $img;
-                       }
-
-                       $dtrail = '';
-
-                       # Set linktype for CSS - if URL==text, link is essentially free
-                       $linktype = ($text == $url) ? 'free' : 'text';
-
-                       # No link text, e.g. [http://domain.tld/some.link]
-                       if ( $text == '' ) {
-                               # Autonumber if allowed. See bug #5918
-                               if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
-                                       $text = '[' . ++$this->mAutonumber . ']';
-                                       $linktype = 'autonumber';
-                               } else {
-                                       # Otherwise just use the URL
-                                       $text = htmlspecialchars( $url );
-                                       $linktype = 'free';
-                               }
-                       } else {
-                               # Have link text, e.g. [http://domain.tld/some.link text]s
-                               # Check for trail
-                               list( $dtrail, $trail ) = Linker::splitTrail( $trail );
-                       }
-
-                       $text = $wgContLang->markNoConversion($text);
-
-                       $url = Sanitizer::cleanUrl( $url );
-
-                       # Process the trail (i.e. everything after this link up until start of the next link),
-                       # replacing any non-bracketed links
-                       $trail = $this->replaceFreeExternalLinks( $trail );
-
-                       # Use the encoded URL
-                       # This means that users can paste URLs directly into the text
-                       # Funny characters like &ouml; aren't valid in URLs anyway
-                       # This was changed in August 2004
-                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
-
-                       # Register link in the output object.
-                       # Replace unnecessary URL escape codes with the referenced character
-                       # This prevents spammers from hiding links from the filters
-                       $pasteurized = Parser::replaceUnusualEscapes( $url );
-                       $this->mOutput->addExternalLink( $pasteurized );
-               }
-
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Replace anything that looks like a URL with a link
-        * @private
-        */
-       function replaceFreeExternalLinks( $text ) {
-               global $wgContLang;
-               $fname = 'Parser::replaceFreeExternalLinks';
-               wfProfileIn( $fname );
-
-               $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-               $s = array_shift( $bits );
-               $i = 0;
-
-               $sk = $this->mOptions->getSkin();
-
-               while ( $i < count( $bits ) ){
-                       $protocol = $bits[$i++];
-                       $remainder = $bits[$i++];
-
-                       $m = array();
-                       if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
-                               # Found some characters after the protocol that look promising
-                               $url = $protocol . $m[1];
-                               $trail = $m[2];
-
-                               # special case: handle urls as url args:
-                               # http://www.example.com/foo?=http://www.example.com/bar
-                               if(strlen($trail) == 0 &&
-                                       isset($bits[$i]) &&
-                                       preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
-                                       preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
-                               {
-                                       # add protocol, arg
-                                       $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
-                                       $i += 2;
-                                       $trail = $m[2];
-                               }
-
-                               # The characters '<' and '>' (which were escaped by
-                               # removeHTMLtags()) should not be included in
-                               # URLs, per RFC 2396.
-                               $m2 = array();
-                               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                                       $trail = substr($url, $m2[0][1]) . $trail;
-                                       $url = substr($url, 0, $m2[0][1]);
-                               }
-
-                               # Move trailing punctuation to $trail
-                               $sep = ',;\.:!?';
-                               # If there is no left bracket, then consider right brackets fair game too
-                               if ( strpos( $url, '(' ) === false ) {
-                                       $sep .= ')';
-                               }
-
-                               $numSepChars = strspn( strrev( $url ), $sep );
-                               if ( $numSepChars ) {
-                                       $trail = substr( $url, -$numSepChars ) . $trail;
-                                       $url = substr( $url, 0, -$numSepChars );
-                               }
-
-                               $url = Sanitizer::cleanUrl( $url );
-
-                               # Is this an external image?
-                               $text = $this->maybeMakeExternalImage( $url );
-                               if ( $text === false ) {
-                                       # Not an image, make a link
-                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
-                                       # Register it in the output object...
-                                       # Replace unnecessary URL escape codes with their equivalent characters
-                                       $pasteurized = Parser::replaceUnusualEscapes( $url );
-                                       $this->mOutput->addExternalLink( $pasteurized );
-                               }
-                               $s .= $text . $trail;
-                       } else {
-                               $s .= $protocol . $remainder;
-                       }
-               }
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Replace unusual URL escape codes with their equivalent characters
-        * @param string
-        * @return string
-        * @static
-        * @todo  This can merge genuinely required bits in the path or query string,
-        *        breaking legit URLs. A proper fix would treat the various parts of
-        *        the URL differently; as a workaround, just use the output for
-        *        statistical records, not for actual linking/output.
-        */
-       static function replaceUnusualEscapes( $url ) {
-               return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
-                       array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
-       }
-
-       /**
-        * Callback function used in replaceUnusualEscapes().
-        * Replaces unusual URL escape codes with their equivalent character
-        * @static
-        * @private
-        */
-       private static function replaceUnusualEscapesCallback( $matches ) {
-               $char = urldecode( $matches[0] );
-               $ord = ord( $char );
-               // Is it an unsafe or HTTP reserved character according to RFC 1738?
-               if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
-                       // No, shouldn't be escaped
-                       return $char;
-               } else {
-                       // Yes, leave it escaped
-                       return $matches[0];
-               }
-       }
-
-       /**
-        * make an image if it's allowed, either through the global
-        * option or through the exception
-        * @private
-        */
-       function maybeMakeExternalImage( $url ) {
-               $sk = $this->mOptions->getSkin();
-               $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
-               $imagesexception = !empty($imagesfrom);
-               $text = false;
-               if ( $this->mOptions->getAllowExternalImages()
-                    || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
-                       if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
-                               # Image found
-                               $text = $sk->makeExternalImage( htmlspecialchars( $url ) );
-                       }
-               }
-               return $text;
-       }
-
-       /**
-        * Process [[ ]] wikilinks
-        *
-        * @private
-        */
-       function replaceInternalLinks( $s ) {
-               global $wgContLang;
-               static $fname = 'Parser::replaceInternalLinks' ;
-
-               wfProfileIn( $fname );
-
-               wfProfileIn( $fname.'-setup' );
-               static $tc = FALSE;
-               # the % is needed to support urlencoded titles as well
-               if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
-
-               $sk = $this->mOptions->getSkin();
-
-               #split the entire text string on occurences of [[
-               $a = explode( '[[', ' ' . $s );
-               #get the first element (all text up to first [[), and remove the space we added
-               $s = array_shift( $a );
-               $s = substr( $s, 1 );
-
-               # Match a link having the form [[namespace:link|alternate]]trail
-               static $e1 = FALSE;
-               if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
-               # Match cases where there is no "]]", which might still be images
-               static $e1_img = FALSE;
-               if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
-
-               $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
-               $e2 = null;
-               if ( $useLinkPrefixExtension ) {
-                       # Match the end of a line for a word that's not followed by whitespace,
-                       # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
-                       $e2 = wfMsgForContent( 'linkprefix' );
-               }
-
-               if( is_null( $this->mTitle ) ) {
-                       wfProfileOut( $fname );
-                       wfProfileOut( $fname.'-setup' );
-                       throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
-               }
-               $nottalk = !$this->mTitle->isTalkPage();
-
-               if ( $useLinkPrefixExtension ) {
-                       $m = array();
-                       if ( preg_match( $e2, $s, $m ) ) {
-                               $first_prefix = $m[2];
-                       } else {
-                               $first_prefix = false;
-                       }
-               } else {
-                       $prefix = '';
-               }
-
-               if($wgContLang->hasVariants()) {
-                       $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
-               } else {
-                       $selflink = array($this->mTitle->getPrefixedText());
-               }
-               $useSubpages = $this->areSubpagesAllowed();
-               wfProfileOut( $fname.'-setup' );
-
-               # Loop for each link
-               for ($k = 0; isset( $a[$k] ); $k++) {
-                       $line = $a[$k];
-                       if ( $useLinkPrefixExtension ) {
-                               wfProfileIn( $fname.'-prefixhandling' );
-                               if ( preg_match( $e2, $s, $m ) ) {
-                                       $prefix = $m[2];
-                                       $s = $m[1];
-                               } else {
-                                       $prefix='';
-                               }
-                               # first link
-                               if($first_prefix) {
-                                       $prefix = $first_prefix;
-                                       $first_prefix = false;
-                               }
-                               wfProfileOut( $fname.'-prefixhandling' );
-                       }
-
-                       $might_be_img = false;
-
-                       wfProfileIn( "$fname-e1" );
-                       if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
-                               $text = $m[2];
-                               # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
-                               # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
-                               # the real problem is with the $e1 regex
-                               # See bug 1300.
-                               #
-                               # Still some problems for cases where the ] is meant to be outside punctuation,
-                               # and no image is in sight. See bug 2095.
-                               #
-                               if( $text !== '' &&
-                                       substr( $m[3], 0, 1 ) === ']' &&
-                                       strpos($text, '[') !== false
-                               )
-                               {
-                                       $text .= ']'; # so that replaceExternalLinks($text) works later
-                                       $m[3] = substr( $m[3], 1 );
-                               }
-                               # fix up urlencoded title texts
-                               if( strpos( $m[1], '%' ) !== false ) {
-                                       # Should anchors '#' also be rejected?
-                                       $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
-                               }
-                               $trail = $m[3];
-                       } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
-                               $might_be_img = true;
-                               $text = $m[2];
-                               if ( strpos( $m[1], '%' ) !== false ) {
-                                       $m[1] = urldecode($m[1]);
-                               }
-                               $trail = "";
-                       } else { # Invalid form; output directly
-                               $s .= $prefix . '[[' . $line ;
-                               wfProfileOut( "$fname-e1" );
-                               continue;
-                       }
-                       wfProfileOut( "$fname-e1" );
-                       wfProfileIn( "$fname-misc" );
-
-                       # Don't allow internal links to pages containing
-                       # PROTO: where PROTO is a valid URL protocol; these
-                       # should be external links.
-                       if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
-                               $s .= $prefix . '[[' . $line ;
-                               wfProfileOut( "$fname-misc" );
-                               continue;
-                       }
-
-                       # Make subpage if necessary
-                       if( $useSubpages ) {
-                               $link = $this->maybeDoSubpageLink( $m[1], $text );
-                       } else {
-                               $link = $m[1];
-                       }
-
-                       $noforce = (substr($m[1], 0, 1) != ':');
-                       if (!$noforce) {
-                               # Strip off leading ':'
-                               $link = substr($link, 1);
-                       }
-
-                       wfProfileOut( "$fname-misc" );
-                       wfProfileIn( "$fname-title" );
-                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
-                       if( !$nt ) {
-                               $s .= $prefix . '[[' . $line;
-                               wfProfileOut( "$fname-title" );
-                               continue;
-                       }
-
-                       $ns = $nt->getNamespace();
-                       $iw = $nt->getInterWiki();
-                       wfProfileOut( "$fname-title" );
-
-                       if ($might_be_img) { # if this is actually an invalid link
-                               wfProfileIn( "$fname-might_be_img" );
-                               if ($ns == NS_IMAGE && $noforce) { #but might be an image
-                                       $found = false;
-                                       while (isset ($a[$k+1]) ) {
-                                               #look at the next 'line' to see if we can close it there
-                                               $spliced = array_splice( $a, $k + 1, 1 );
-                                               $next_line = array_shift( $spliced );
-                                               $m = explode( ']]', $next_line, 3 );
-                                               if ( count( $m ) == 3 ) {
-                                                       # the first ]] closes the inner link, the second the image
-                                                       $found = true;
-                                                       $text .= "[[{$m[0]}]]{$m[1]}";
-                                                       $trail = $m[2];
-                                                       break;
-                                               } elseif ( count( $m ) == 2 ) {
-                                                       #if there's exactly one ]] that's fine, we'll keep looking
-                                                       $text .= "[[{$m[0]}]]{$m[1]}";
-                                               } else {
-                                                       #if $next_line is invalid too, we need look no further
-                                                       $text .= '[[' . $next_line;
-                                                       break;
-                                               }
-                                       }
-                                       if ( !$found ) {
-                                               # we couldn't find the end of this imageLink, so output it raw
-                                               #but don't ignore what might be perfectly normal links in the text we've examined
-                                               $text = $this->replaceInternalLinks($text);
-                                               $s .= "{$prefix}[[$link|$text";
-                                               # note: no $trail, because without an end, there *is* no trail
-                                               wfProfileOut( "$fname-might_be_img" );
-                                               continue;
-                                       }
-                               } else { #it's not an image, so output it raw
-                                       $s .= "{$prefix}[[$link|$text";
-                                       # note: no $trail, because without an end, there *is* no trail
-                                       wfProfileOut( "$fname-might_be_img" );
-                                       continue;
-                               }
-                               wfProfileOut( "$fname-might_be_img" );
-                       }
-
-                       $wasblank = ( '' == $text );
-                       if( $wasblank ) $text = $link;
-
-                       # Link not escaped by : , create the various objects
-                       if( $noforce ) {
-
-                               # Interwikis
-                               wfProfileIn( "$fname-interwiki" );
-                               if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
-                                       $this->mOutput->addLanguageLink( $nt->getFullText() );
-                                       $s = rtrim($s . $prefix);
-                                       $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
-                                       wfProfileOut( "$fname-interwiki" );
-                                       continue;
-                               }
-                               wfProfileOut( "$fname-interwiki" );
-
-                               if ( $ns == NS_IMAGE ) {
-                                       wfProfileIn( "$fname-image" );
-                                       if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
-                                               # recursively parse links inside the image caption
-                                               # actually, this will parse them in any other parameters, too,
-                                               # but it might be hard to fix that, and it doesn't matter ATM
-                                               $text = $this->replaceExternalLinks($text);
-                                               $text = $this->replaceInternalLinks($text);
-
-                                               # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
-                                               $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
-                                               $this->mOutput->addImage( $nt->getDBkey() );
-
-                                               wfProfileOut( "$fname-image" );
-                                               continue;
-                                       } else {
-                                               # We still need to record the image's presence on the page
-                                               $this->mOutput->addImage( $nt->getDBkey() );
-                                       }
-                                       wfProfileOut( "$fname-image" );
-
-                               }
-
-                               if ( $ns == NS_CATEGORY ) {
-                                       wfProfileIn( "$fname-category" );
-                                       $s = rtrim($s . "\n"); # bug 87
-
-                                       if ( $wasblank ) {
-                                               $sortkey = $this->getDefaultSort();
-                                       } else {
-                                               $sortkey = $text;
-                                       }
-                                       $sortkey = Sanitizer::decodeCharReferences( $sortkey );
-                                       $sortkey = str_replace( "\n", '', $sortkey );
-                                       $sortkey = $wgContLang->convertCategoryKey( $sortkey );
-                                       $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
-
-                                       /**
-                                        * Strip the whitespace Category links produce, see bug 87
-                                        * @todo We might want to use trim($tmp, "\n") here.
-                                        */
-                                       $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
-
-                                       wfProfileOut( "$fname-category" );
-                                       continue;
-                               }
-                       }
-
-                       # Self-link checking
-                       if( $nt->getFragment() === '' ) {
-                               if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
-                                       $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
-                                       continue;
-                               }
-                       }
-
-                       # Special and Media are pseudo-namespaces; no pages actually exist in them
-                       if( $ns == NS_MEDIA ) {
-                               # Give extensions a chance to select the file revision for us
-                               $skip = $time = false;
-                               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
-                               if ( $skip ) {
-                                       $link = $sk->makeLinkObj( $nt );
-                               } else {
-                                       $link = $sk->makeMediaLinkObj( $nt, $text, $time );
-                               }
-                               # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
-                               $s .= $prefix . $this->armorLinks( $link ) . $trail;
-                               $this->mOutput->addImage( $nt->getDBkey() );
-                               continue;
-                       } elseif( $ns == NS_SPECIAL ) {
-                               if( SpecialPage::exists( $nt->getDBkey() ) ) {
-                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
-                               } else {
-                                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
-                               }
-                               continue;
-                       } elseif( $ns == NS_IMAGE ) {
-                               $img = wfFindFile( $nt );
-                               if( $img ) {
-                                       // Force a blue link if the file exists; may be a remote
-                                       // upload on the shared repository, and we want to see its
-                                       // auto-generated page.
-                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
-                                       $this->mOutput->addLink( $nt );
-                                       continue;
-                               }
-                       }
-                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
-               }
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Make a link placeholder. The text returned can be later resolved to a real link with
-        * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
-        * parsing of interwiki links, and secondly to allow all existence checks and
-        * article length checks (for stub links) to be bundled into a single query.
-        *
-        */
-       function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-               wfProfileIn( __METHOD__ );
-               if ( ! is_object($nt) ) {
-                       # Fail gracefully
-                       $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
-               } else {
-                       # Separate the link trail from the rest of the link
-                       list( $inside, $trail ) = Linker::splitTrail( $trail );
-
-                       if ( $nt->isExternal() ) {
-                               $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
-                               $this->mInterwikiLinkHolders['titles'][] = $nt;
-                               $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
-                       } else {
-                               $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
-                               $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
-                               $this->mLinkHolders['queries'][] = $query;
-                               $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
-                               $this->mLinkHolders['titles'][] = $nt;
-
-                               $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-               return $retVal;
-       }
-
-       /**
-        * Render a forced-blue link inline; protect against double expansion of
-        * URLs if we're in a mode that prepends full URL prefixes to internal links.
-        * Since this little disaster has to split off the trail text to avoid
-        * breaking URLs in the following text without breaking trails on the
-        * wiki links, it's been made into a horrible function.
-        *
-        * @param Title $nt
-        * @param string $text
-        * @param string $query
-        * @param string $trail
-        * @param string $prefix
-        * @return string HTML-wikitext mix oh yuck
-        */
-       function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-               list( $inside, $trail ) = Linker::splitTrail( $trail );
-               $sk = $this->mOptions->getSkin();
-               $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
-               return $this->armorLinks( $link ) . $trail;
-       }
-
-       /**
-        * Insert a NOPARSE hacky thing into any inline links in a chunk that's
-        * going to go through further parsing steps before inline URL expansion.
-        *
-        * In particular this is important when using action=render, which causes
-        * full URLs to be included.
-        *
-        * Oh man I hate our multi-layer parser!
-        *
-        * @param string more-or-less HTML
-        * @return string less-or-more HTML with NOPARSE bits
-        */
-       function armorLinks( $text ) {
-               return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
-                       "{$this->mUniqPrefix}NOPARSE$1", $text );
-       }
-
-       /**
-        * Return true if subpage links should be expanded on this page.
-        * @return bool
-        */
-       function areSubpagesAllowed() {
-               # Some namespaces don't allow subpages
-               return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
-       }
-
-       /**
-        * Handle link to subpage if necessary
-        * @param string $target the source of the link
-        * @param string &$text the link text, modified as necessary
-        * @return string the full name of the link
-        * @private
-        */
-       function maybeDoSubpageLink($target, &$text) {
-               # Valid link forms:
-               # Foobar -- normal
-               # :Foobar -- override special treatment of prefix (images, language links)
-               # /Foobar -- convert to CurrentPage/Foobar
-               # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
-               # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
-               # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
-
-               $fname = 'Parser::maybeDoSubpageLink';
-               wfProfileIn( $fname );
-               $ret = $target; # default return value is no change
-
-               # Some namespaces don't allow subpages,
-               # so only perform processing if subpages are allowed
-               if( $this->areSubpagesAllowed() ) {
-                       $hash = strpos( $target, '#' );
-                       if( $hash !== false ) {
-                               $suffix = substr( $target, $hash );
-                               $target = substr( $target, 0, $hash );
-                       } else {
-                               $suffix = '';
-                       }
-                       # bug 7425
-                       $target = trim( $target );
-                       # Look at the first character
-                       if( $target != '' && $target{0} == '/' ) {
-                               # / at end means we don't want the slash to be shown
-                               $m = array();
-                               $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
-                               if( $trailingSlashes ) {
-                                       $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
-                               } else {
-                                       $noslash = substr( $target, 1 );
-                               }
-
-                               $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
-                               if( '' === $text ) {
-                                       $text = $target . $suffix;
-                               } # this might be changed for ugliness reasons
-                       } else {
-                               # check for .. subpage backlinks
-                               $dotdotcount = 0;
-                               $nodotdot = $target;
-                               while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
-                                       ++$dotdotcount;
-                                       $nodotdot = substr( $nodotdot, 3 );
-                               }
-                               if($dotdotcount > 0) {
-                                       $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
-                                       if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
-                                               $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
-                                               # / at the end means don't show full path
-                                               if( substr( $nodotdot, -1, 1 ) == '/' ) {
-                                                       $nodotdot = substr( $nodotdot, 0, -1 );
-                                                       if( '' === $text ) {
-                                                               $text = $nodotdot . $suffix;
-                                                       }
-                                               }
-                                               $nodotdot = trim( $nodotdot );
-                                               if( $nodotdot != '' ) {
-                                                       $ret .= '/' . $nodotdot;
-                                               }
-                                               $ret .= $suffix;
-                                       }
-                               }
-                       }
-               }
-
-               wfProfileOut( $fname );
-               return $ret;
-       }
-
-       /**#@+
-        * Used by doBlockLevels()
-        * @private
-        */
-       /* private */ function closeParagraph() {
-               $result = '';
-               if ( '' != $this->mLastSection ) {
-                       $result = '</' . $this->mLastSection  . ">\n";
-               }
-               $this->mInPre = false;
-               $this->mLastSection = '';
-               return $result;
-       }
-       # getCommon() returns the length of the longest common substring
-       # of both arguments, starting at the beginning of both.
-       #
-       /* private */ function getCommon( $st1, $st2 ) {
-               $fl = strlen( $st1 );
-               $shorter = strlen( $st2 );
-               if ( $fl < $shorter ) { $shorter = $fl; }
-
-               for ( $i = 0; $i < $shorter; ++$i ) {
-                       if ( $st1{$i} != $st2{$i} ) { break; }
-               }
-               return $i;
-       }
-       # These next three functions open, continue, and close the list
-       # element appropriate to the prefix character passed into them.
-       #
-       /* private */ function openList( $char ) {
-               $result = $this->closeParagraph();
-
-               if ( '*' == $char ) { $result .= '<ul><li>'; }
-               else if ( '#' == $char ) { $result .= '<ol><li>'; }
-               else if ( ':' == $char ) { $result .= '<dl><dd>'; }
-               else if ( ';' == $char ) {
-                       $result .= '<dl><dt>';
-                       $this->mDTopen = true;
-               }
-               else { $result = '<!-- ERR 1 -->'; }
-
-               return $result;
-       }
-
-       /* private */ function nextItem( $char ) {
-               if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
-               else if ( ':' == $char || ';' == $char ) {
-                       $close = '</dd>';
-                       if ( $this->mDTopen ) { $close = '</dt>'; }
-                       if ( ';' == $char ) {
-                               $this->mDTopen = true;
-                               return $close . '<dt>';
-                       } else {
-                               $this->mDTopen = false;
-                               return $close . '<dd>';
-                       }
-               }
-               return '<!-- ERR 2 -->';
-       }
-
-       /* private */ function closeList( $char ) {
-               if ( '*' == $char ) { $text = '</li></ul>'; }
-               else if ( '#' == $char ) { $text = '</li></ol>'; }
-               else if ( ':' == $char ) {
-                       if ( $this->mDTopen ) {
-                               $this->mDTopen = false;
-                               $text = '</dt></dl>';
-                       } else {
-                               $text = '</dd></dl>';
-                       }
-               }
-               else {  return '<!-- ERR 3 -->'; }
-               return $text."\n";
-       }
-       /**#@-*/
-
-       /**
-        * Make lists from lines starting with ':', '*', '#', etc.
-        *
-        * @private
-        * @return string the lists rendered as HTML
-        */
-       function doBlockLevels( $text, $linestart ) {
-               $fname = 'Parser::doBlockLevels';
-               wfProfileIn( $fname );
-
-               # Parsing through the text line by line.  The main thing
-               # happening here is handling of block-level elements p, pre,
-               # and making lists from lines starting with * # : etc.
-               #
-               $textLines = explode( "\n", $text );
-
-               $lastPrefix = $output = '';
-               $this->mDTopen = $inBlockElem = false;
-               $prefixLength = 0;
-               $paragraphStack = false;
-
-               if ( !$linestart ) {
-                       $output .= array_shift( $textLines );
-               }
-               foreach ( $textLines as $oLine ) {
-                       $lastPrefixLength = strlen( $lastPrefix );
-                       $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
-                       $preOpenMatch = preg_match('/<pre/i', $oLine );
-                       if ( !$this->mInPre ) {
-                               # Multiple prefixes may abut each other for nested lists.
-                               $prefixLength = strspn( $oLine, '*#:;' );
-                               $pref = substr( $oLine, 0, $prefixLength );
-
-                               # eh?
-                               $pref2 = str_replace( ';', ':', $pref );
-                               $t = substr( $oLine, $prefixLength );
-                               $this->mInPre = !empty($preOpenMatch);
-                       } else {
-                               # Don't interpret any other prefixes in preformatted text
-                               $prefixLength = 0;
-                               $pref = $pref2 = '';
-                               $t = $oLine;
-                       }
-
-                       # List generation
-                       if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
-                               # Same as the last item, so no need to deal with nesting or opening stuff
-                               $output .= $this->nextItem( substr( $pref, -1 ) );
-                               $paragraphStack = false;
-
-                               if ( substr( $pref, -1 ) == ';') {
-                                       # The one nasty exception: definition lists work like this:
-                                       # ; title : definition text
-                                       # So we check for : in the remainder text to split up the
-                                       # title and definition, without b0rking links.
-                                       $term = $t2 = '';
-                                       if ($this->findColonNoLinks($t, $term, $t2) !== false) {
-                                               $t = $t2;
-                                               $output .= $term . $this->nextItem( ':' );
-                                       }
-                               }
-                       } elseif( $prefixLength || $lastPrefixLength ) {
-                               # Either open or close a level...
-                               $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
-                               $paragraphStack = false;
-
-                               while( $commonPrefixLength < $lastPrefixLength ) {
-                                       $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
-                                       --$lastPrefixLength;
-                               }
-                               if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
-                                       $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
-                               }
-                               while ( $prefixLength > $commonPrefixLength ) {
-                                       $char = substr( $pref, $commonPrefixLength, 1 );
-                                       $output .= $this->openList( $char );
-
-                                       if ( ';' == $char ) {
-                                               # FIXME: This is dupe of code above
-                                               if ($this->findColonNoLinks($t, $term, $t2) !== false) {
-                                                       $t = $t2;
-                                                       $output .= $term . $this->nextItem( ':' );
-                                               }
-                                       }
-                                       ++$commonPrefixLength;
-                               }
-                               $lastPrefix = $pref2;
-                       }
-                       if( 0 == $prefixLength ) {
-                               wfProfileIn( "$fname-paragraph" );
-                               # No prefix (not in list)--go to paragraph mode
-                               // XXX: use a stack for nestable elements like span, table and div
-                               $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
-                               $closematch = preg_match(
-                                       '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
-                                       '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
-                               if ( $openmatch or $closematch ) {
-                                       $paragraphStack = false;
-                                       # TODO bug 5718: paragraph closed
-                                       $output .= $this->closeParagraph();
-                                       if ( $preOpenMatch and !$preCloseMatch ) {
-                                               $this->mInPre = true;
-                                       }
-                                       if ( $closematch ) {
-                                               $inBlockElem = false;
-                                       } else {
-                                               $inBlockElem = true;
-                                       }
-                               } else if ( !$inBlockElem && !$this->mInPre ) {
-                                       if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
-                                               // pre
-                                               if ($this->mLastSection != 'pre') {
-                                                       $paragraphStack = false;
-                                                       $output .= $this->closeParagraph().'<pre>';
-                                                       $this->mLastSection = 'pre';
-                                               }
-                                               $t = substr( $t, 1 );
-                                       } else {
-                                               // paragraph
-                                               if ( '' == trim($t) ) {
-                                                       if ( $paragraphStack ) {
-                                                               $output .= $paragraphStack.'<br />';
-                                                               $paragraphStack = false;
-                                                               $this->mLastSection = 'p';
-                                                       } else {
-                                                               if ($this->mLastSection != 'p' ) {
-                                                                       $output .= $this->closeParagraph();
-                                                                       $this->mLastSection = '';
-                                                                       $paragraphStack = '<p>';
-                                                               } else {
-                                                                       $paragraphStack = '</p><p>';
-                                                               }
-                                                       }
-                                               } else {
-                                                       if ( $paragraphStack ) {
-                                                               $output .= $paragraphStack;
-                                                               $paragraphStack = false;
-                                                               $this->mLastSection = 'p';
-                                                       } else if ($this->mLastSection != 'p') {
-                                                               $output .= $this->closeParagraph().'<p>';
-                                                               $this->mLastSection = 'p';
-                                                       }
-                                               }
-                                       }
-                               }
-                               wfProfileOut( "$fname-paragraph" );
-                       }
-                       // somewhere above we forget to get out of pre block (bug 785)
-                       if($preCloseMatch && $this->mInPre) {
-                               $this->mInPre = false;
-                       }
-                       if ($paragraphStack === false) {
-                               $output .= $t."\n";
-                       }
-               }
-               while ( $prefixLength ) {
-                       $output .= $this->closeList( $pref2{$prefixLength-1} );
-                       --$prefixLength;
-               }
-               if ( '' != $this->mLastSection ) {
-                       $output .= '</' . $this->mLastSection . '>';
-                       $this->mLastSection = '';
-               }
-
-               wfProfileOut( $fname );
-               return $output;
-       }
-
-       /**
-        * Split up a string on ':', ignoring any occurences inside tags
-        * to prevent illegal overlapping.
-        * @param string $str the string to split
-        * @param string &$before set to everything before the ':'
-        * @param string &$after set to everything after the ':'
-        * return string the position of the ':', or false if none found
-        */
-       function findColonNoLinks($str, &$before, &$after) {
-               $fname = 'Parser::findColonNoLinks';
-               wfProfileIn( $fname );
-
-               $pos = strpos( $str, ':' );
-               if( $pos === false ) {
-                       // Nothing to find!
-                       wfProfileOut( $fname );
-                       return false;
-               }
-
-               $lt = strpos( $str, '<' );
-               if( $lt === false || $lt > $pos ) {
-                       // Easy; no tag nesting to worry about
-                       $before = substr( $str, 0, $pos );
-                       $after = substr( $str, $pos+1 );
-                       wfProfileOut( $fname );
-                       return $pos;
-               }
-
-               // Ugly state machine to walk through avoiding tags.
-               $state = self::COLON_STATE_TEXT;
-               $stack = 0;
-               $len = strlen( $str );
-               for( $i = 0; $i < $len; $i++ ) {
-                       $c = $str{$i};
-
-                       switch( $state ) {
-                       // (Using the number is a performance hack for common cases)
-                       case 0: // self::COLON_STATE_TEXT:
-                               switch( $c ) {
-                               case "<":
-                                       // Could be either a <start> tag or an </end> tag
-                                       $state = self::COLON_STATE_TAGSTART;
-                                       break;
-                               case ":":
-                                       if( $stack == 0 ) {
-                                               // We found it!
-                                               $before = substr( $str, 0, $i );
-                                               $after = substr( $str, $i + 1 );
-                                               wfProfileOut( $fname );
-                                               return $i;
-                                       }
-                                       // Embedded in a tag; don't break it.
-                                       break;
-                               default:
-                                       // Skip ahead looking for something interesting
-                                       $colon = strpos( $str, ':', $i );
-                                       if( $colon === false ) {
-                                               // Nothing else interesting
-                                               wfProfileOut( $fname );
-                                               return false;
-                                       }
-                                       $lt = strpos( $str, '<', $i );
-                                       if( $stack === 0 ) {
-                                               if( $lt === false || $colon < $lt ) {
-                                                       // We found it!
-                                                       $before = substr( $str, 0, $colon );
-                                                       $after = substr( $str, $colon + 1 );
-                                                       wfProfileOut( $fname );
-                                                       return $i;
-                                               }
-                                       }
-                                       if( $lt === false ) {
-                                               // Nothing else interesting to find; abort!
-                                               // We're nested, but there's no close tags left. Abort!
-                                               break 2;
-                                       }
-                                       // Skip ahead to next tag start
-                                       $i = $lt;
-                                       $state = self::COLON_STATE_TAGSTART;
-                               }
-                               break;
-                       case 1: // self::COLON_STATE_TAG:
-                               // In a <tag>
-                               switch( $c ) {
-                               case ">":
-                                       $stack++;
-                                       $state = self::COLON_STATE_TEXT;
-                                       break;
-                               case "/":
-                                       // Slash may be followed by >?
-                                       $state = self::COLON_STATE_TAGSLASH;
-                                       break;
-                               default:
-                                       // ignore
-                               }
-                               break;
-                       case 2: // self::COLON_STATE_TAGSTART:
-                               switch( $c ) {
-                               case "/":
-                                       $state = self::COLON_STATE_CLOSETAG;
-                                       break;
-                               case "!":
-                                       $state = self::COLON_STATE_COMMENT;
-                                       break;
-                               case ">":
-                                       // Illegal early close? This shouldn't happen D:
-                                       $state = self::COLON_STATE_TEXT;
-                                       break;
-                               default:
-                                       $state = self::COLON_STATE_TAG;
-                               }
-                               break;
-                       case 3: // self::COLON_STATE_CLOSETAG:
-                               // In a </tag>
-                               if( $c == ">" ) {
-                                       $stack--;
-                                       if( $stack < 0 ) {
-                                               wfDebug( "Invalid input in $fname; too many close tags\n" );
-                                               wfProfileOut( $fname );
-                                               return false;
-                                       }
-                                       $state = self::COLON_STATE_TEXT;
-                               }
-                               break;
-                       case self::COLON_STATE_TAGSLASH:
-                               if( $c == ">" ) {
-                                       // Yes, a self-closed tag <blah/>
-                                       $state = self::COLON_STATE_TEXT;
-                               } else {
-                                       // Probably we're jumping the gun, and this is an attribute
-                                       $state = self::COLON_STATE_TAG;
-                               }
-                               break;
-                       case 5: // self::COLON_STATE_COMMENT:
-                               if( $c == "-" ) {
-                                       $state = self::COLON_STATE_COMMENTDASH;
-                               }
-                               break;
-                       case self::COLON_STATE_COMMENTDASH:
-                               if( $c == "-" ) {
-                                       $state = self::COLON_STATE_COMMENTDASHDASH;
-                               } else {
-                                       $state = self::COLON_STATE_COMMENT;
-                               }
-                               break;
-                       case self::COLON_STATE_COMMENTDASHDASH:
-                               if( $c == ">" ) {
-                                       $state = self::COLON_STATE_TEXT;
-                               } else {
-                                       $state = self::COLON_STATE_COMMENT;
-                               }
-                               break;
-                       default:
-                               throw new MWException( "State machine error in $fname" );
-                       }
-               }
-               if( $stack > 0 ) {
-                       wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
-                       return false;
-               }
-               wfProfileOut( $fname );
-               return false;
-       }
-
-       /**
-        * Return value of a magic variable (like PAGENAME)
-        *
-        * @private
-        */
-       function getVariableValue( $index ) {
-               global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
-
-               /**
-                * Some of these require message or data lookups and can be
-                * expensive to check many times.
-                */
-               if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
-                       if ( isset( $this->mVarCache[$index] ) ) {
-                               return $this->mVarCache[$index];
-                       }
-               }
-
-               $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
-               wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
-
-               # Use the time zone
-               global $wgLocaltimezone;
-               if ( isset( $wgLocaltimezone ) ) {
-                       $oldtz = getenv( 'TZ' );
-                       putenv( 'TZ='.$wgLocaltimezone );
-               }
-
-               wfSuppressWarnings(); // E_STRICT system time bitching
-               $localTimestamp = date( 'YmdHis', $ts );
-               $localMonth = date( 'm', $ts );
-               $localMonthName = date( 'n', $ts );
-               $localDay = date( 'j', $ts );
-               $localDay2 = date( 'd', $ts );
-               $localDayOfWeek = date( 'w', $ts );
-               $localWeek = date( 'W', $ts );
-               $localYear = date( 'Y', $ts );
-               $localHour = date( 'H', $ts );
-               if ( isset( $wgLocaltimezone ) ) {
-                       putenv( 'TZ='.$oldtz );
-               }
-               wfRestoreWarnings();
-
-               switch ( $index ) {
-                       case 'currentmonth':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
-                       case 'currentmonthname':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
-                       case 'currentmonthnamegen':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
-                       case 'currentmonthabbrev':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
-                       case 'currentday':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
-                       case 'currentday2':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
-                       case 'localmonth':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
-                       case 'localmonthname':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
-                       case 'localmonthnamegen':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
-                       case 'localmonthabbrev':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
-                       case 'localday':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
-                       case 'localday2':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 );
-                       case 'pagename':
-                               return wfEscapeWikiText( $this->mTitle->getText() );
-                       case 'pagenamee':
-                               return $this->mTitle->getPartialURL();
-                       case 'fullpagename':
-                               return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
-                       case 'fullpagenamee':
-                               return $this->mTitle->getPrefixedURL();
-                       case 'subpagename':
-                               return wfEscapeWikiText( $this->mTitle->getSubpageText() );
-                       case 'subpagenamee':
-                               return $this->mTitle->getSubpageUrlForm();
-                       case 'basepagename':
-                               return wfEscapeWikiText( $this->mTitle->getBaseText() );
-                       case 'basepagenamee':
-                               return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
-                       case 'talkpagename':
-                               if( $this->mTitle->canTalk() ) {
-                                       $talkPage = $this->mTitle->getTalkPage();
-                                       return wfEscapeWikiText( $talkPage->getPrefixedText() );
-                               } else {
-                                       return '';
-                               }
-                       case 'talkpagenamee':
-                               if( $this->mTitle->canTalk() ) {
-                                       $talkPage = $this->mTitle->getTalkPage();
-                                       return $talkPage->getPrefixedUrl();
-                               } else {
-                                       return '';
-                               }
-                       case 'subjectpagename':
-                               $subjPage = $this->mTitle->getSubjectPage();
-                               return wfEscapeWikiText( $subjPage->getPrefixedText() );
-                       case 'subjectpagenamee':
-                               $subjPage = $this->mTitle->getSubjectPage();
-                               return $subjPage->getPrefixedUrl();
-                       case 'revisionid':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
-                               return $this->mRevisionId;
-                       case 'revisionday':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
-                               return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
-                       case 'revisionday2':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
-                               return substr( $this->getRevisionTimestamp(), 6, 2 );
-                       case 'revisionmonth':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
-                               return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
-                       case 'revisionyear':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
-                               return substr( $this->getRevisionTimestamp(), 0, 4 );
-                       case 'revisiontimestamp':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
-                               $this->mOutput->setFlag( 'vary-revision' );
-                               wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
-                               return $this->getRevisionTimestamp();
-                       case 'namespace':
-                               return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
-                       case 'namespacee':
-                               return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
-                       case 'talkspace':
-                               return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
-                       case 'talkspacee':
-                               return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
-                       case 'subjectspace':
-                               return $this->mTitle->getSubjectNsText();
-                       case 'subjectspacee':
-                               return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
-                       case 'currentdayname':
-                               return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
-                       case 'currentyear':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
-                       case 'currenttime':
-                               return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
-                       case 'currenthour':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
-                       case 'currentweek':
-                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
-                               // int to remove the padding
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
-                       case 'currentdow':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
-                       case 'localdayname':
-                               return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
-                       case 'localyear':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
-                       case 'localtime':
-                               return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
-                       case 'localhour':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localHour, true );
-                       case 'localweek':
-                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
-                               // int to remove the padding
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek );
-                       case 'localdow':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
-                       case 'numberofarticles':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
-                       case 'numberoffiles':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
-                       case 'numberofusers':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
-                       case 'numberofpages':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
-                       case 'numberofadmins':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() );
-                       case 'numberofedits':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
-                       case 'currenttimestamp':
-                               return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
-                       case 'localtimestamp':
-                               return $this->mVarCache[$index] = $localTimestamp;
-                       case 'currentversion':
-                               return $this->mVarCache[$index] = SpecialVersion::getVersion();
-                       case 'sitename':
-                               return $wgSitename;
-                       case 'server':
-                               return $wgServer;
-                       case 'servername':
-                               return $wgServerName;
-                       case 'scriptpath':
-                               return $wgScriptPath;
-                       case 'directionmark':
-                               return $wgContLang->getDirMark();
-                       case 'contentlanguage':
-                               global $wgContLanguageCode;
-                               return $wgContLanguageCode;
-                       default:
-                               $ret = null;
-                               if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) )
-                                       return $ret;
-                               else
-                                       return null;
-               }
-       }
-
-       /**
-        * initialise the magic variables (like CURRENTMONTHNAME)
-        *
-        * @private
-        */
-       function initialiseVariables() {
-               $fname = 'Parser::initialiseVariables';
-               wfProfileIn( $fname );
-               $variableIDs = MagicWord::getVariableIDs();
-
-               $this->mVariables = new MagicWordArray( $variableIDs );
-               wfProfileOut( $fname );
-       }
-
-       /**
-        * Preprocess some wikitext and return the document tree.
-        * This is the ghost of replace_variables().
-        *
-        * @param string $text The text to parse
-        * @param integer flags Bitwise combination of:
-        *          self::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
-        *                                     included. Default is to assume a direct page view.
-        *
-        * The generated DOM tree must depend only on the input text and the flags.
-        * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
-        *
-        * Any flag added to the $flags parameter here, or any other parameter liable to cause a
-        * change in the DOM tree for a given text, must be passed through the section identifier
-        * in the section edit link and thus back to extractSections().
-        *
-        * The output of this function is currently only cached in process memory, but a persistent
-        * cache may be implemented at a later date which takes further advantage of these strict
-        * dependency requirements.
-        *
-        * @private
-        */
-       function preprocessToDom ( $text, $flags = 0 ) {
-               $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
-               return $dom;
-       }
-
-       /*
-        * Return a three-element array: leading whitespace, string contents, trailing whitespace
-        */
-       public static function splitWhitespace( $s ) {
-               $ltrimmed = ltrim( $s );
-               $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
-               $trimmed = rtrim( $ltrimmed );
-               $diff = strlen( $ltrimmed ) - strlen( $trimmed );
-               if ( $diff > 0 ) {
-                       $w2 = substr( $ltrimmed, -$diff );
-               } else {
-                       $w2 = '';
-               }
-               return array( $w1, $trimmed, $w2 );
-       }
-
-       /**
-        * Replace magic variables, templates, and template arguments
-        * with the appropriate text. Templates are substituted recursively,
-        * taking care to avoid infinite loops.
-        *
-        * Note that the substitution depends on value of $mOutputType:
-        *  self::OT_WIKI: only {{subst:}} templates
-        *  self::OT_PREPROCESS: templates but not extension tags
-        *  self::OT_HTML: all templates and extension tags
-        *
-        * @param string $tex The text to transform
-        * @param PPFrame $frame Object describing the arguments passed to the template
-        * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
-        * @private
-        */
-       function replaceVariables( $text, $frame = false, $argsOnly = false ) {
-               # Prevent too big inclusions
-               if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
-                       return $text;
-               }
-
-               $fname = __METHOD__;
-               wfProfileIn( $fname );
-
-               if ( $frame === false ) {
-                       $frame = $this->getPreprocessor()->newFrame();
-               } elseif ( !( $frame instanceof PPFrame ) ) {
-                       throw new MWException( __METHOD__ . ' called using the old argument format' );
-               }
-
-               $dom = $this->preprocessToDom( $text );
-               $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
-               $text = $frame->expand( $dom, $flags );
-
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
-       static function createAssocArgs( $args ) {
-               $assocArgs = array();
-               $index = 1;
-               foreach( $args as $arg ) {
-                       $eqpos = strpos( $arg, '=' );
-                       if ( $eqpos === false ) {
-                               $assocArgs[$index++] = $arg;
-                       } else {
-                               $name = trim( substr( $arg, 0, $eqpos ) );
-                               $value = trim( substr( $arg, $eqpos+1 ) );
-                               if ( $value === false ) {
-                                       $value = '';
-                               }
-                               if ( $name !== false ) {
-                                       $assocArgs[$name] = $value;
-                               }
-                       }
-               }
-
-               return $assocArgs;
-       }
-
-       /**
-        * Warn the user when a parser limitation is reached
-        * Will warn at most once the user per limitation type
-        *
-        * @param string $limitationType, should be one of:
-        *   'expensive-parserfunction' (corresponding messages: 'expensive-parserfunction-warning', 'expensive-parserfunction-category')
-        *   'post-expand-template-argument' (corresponding messages: 'post-expand-template-argument-warning', 'post-expand-template-argument-category')
-        *   'post-expand-template-inclusion' (corresponding messages: 'post-expand-template-inclusion-warning', 'post-expand-template-inclusion-category')
-        * @params int $current, $max When an explicit limit has been
-        *       exceeded, provide the values (optional)
-        */
-       function limitationWarn( $limitationType, $current=null, $max=null) {
-               $msgName = $limitationType . '-warning';
-               //does no harm if $current and $max are present but are unnecessary for the message
-               $warning = wfMsg( $msgName, $current, $max); 
-               $this->mOutput->addWarning( $warning );
-               $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
-               if ( $cat ) {
-                       $this->mOutput->addCategory( $cat->getDBkey(), $this->getDefaultSort() );
-               }
-       }
-
-       /**
-        * Return the text of a template, after recursively
-        * replacing any variables or templates within the template.
-        *
-        * @param array $piece The parts of the template
-        *  $piece['title']: the title, i.e. the part before the |
-        *  $piece['parts']: the parameter array
-        *  $piece['lineStart']: whether the brace was at the start of a line
-        * @param PPFrame The current frame, contains template arguments
-        * @return string the text of the template
-        * @private
-        */
-       function braceSubstitution( $piece, $frame ) {
-               global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
-               $fname = __METHOD__;
-               wfProfileIn( $fname );
-               wfProfileIn( __METHOD__.'-setup' );
-
-               # Flags
-               $found = false;             # $text has been filled
-               $nowiki = false;            # wiki markup in $text should be escaped
-               $isHTML = false;            # $text is HTML, armour it against wikitext transformation
-               $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
-               $isChildObj = false;        # $text is a DOM node needing expansion in a child frame
-               $isLocalObj = false;        # $text is a DOM node needing expansion in the current frame
-
-               # Title object, where $text came from
-               $title = NULL;
-
-               # $part1 is the bit before the first |, and must contain only title characters.
-               # Various prefixes will be stripped from it later.
-               $titleWithSpaces = $frame->expand( $piece['title'] );
-               $part1 = trim( $titleWithSpaces );
-               $titleText = false;
-
-               # Original title text preserved for various purposes
-               $originalTitle = $part1;
-
-               # $args is a list of argument nodes, starting from index 0, not including $part1
-               $args = (null == $piece['parts']) ? array() : $piece['parts'];
-               wfProfileOut( __METHOD__.'-setup' );
-
-               # SUBST
-               wfProfileIn( __METHOD__.'-modifiers' );
-               if ( !$found ) {
-                       $mwSubst = MagicWord::get( 'subst' );
-                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
-                               # One of two possibilities is true:
-                               # 1) Found SUBST but not in the PST phase
-                               # 2) Didn't find SUBST and in the PST phase
-                               # In either case, return without further processing
-                               $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
-                               $isLocalObj = true;
-                               $found = true;
-                       }
-               }
-
-               # Variables
-               if ( !$found && $args->getLength() == 0 ) {
-                       $id = $this->mVariables->matchStartToEnd( $part1 );
-                       if ( $id !== false ) {
-                               $text = $this->getVariableValue( $id );
-                               if (MagicWord::getCacheTTL($id)>-1)
-                                       $this->mOutput->mContainsOldMagic = true;
-                               $found = true;
-                       }
-               }
-
-               # MSG, MSGNW and RAW
-               if ( !$found ) {
-                       # Check for MSGNW:
-                       $mwMsgnw = MagicWord::get( 'msgnw' );
-                       if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
-                               $nowiki = true;
-                       } else {
-                               # Remove obsolete MSG:
-                               $mwMsg = MagicWord::get( 'msg' );
-                               $mwMsg->matchStartAndRemove( $part1 );
-                       }
-
-                       # Check for RAW:
-                       $mwRaw = MagicWord::get( 'raw' );
-                       if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
-                               $forceRawInterwiki = true;
-                       }
-               }
-               wfProfileOut( __METHOD__.'-modifiers' );
-
-               # Parser functions
-               if ( !$found ) {
-                       wfProfileIn( __METHOD__ . '-pfunc' );
-
-                       $colonPos = strpos( $part1, ':' );
-                       if ( $colonPos !== false ) {
-                               # Case sensitive functions
-                               $function = substr( $part1, 0, $colonPos );
-                               if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
-                                       $function = $this->mFunctionSynonyms[1][$function];
-                               } else {
-                                       # Case insensitive functions
-                                       $function = strtolower( $function );
-                                       if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
-                                               $function = $this->mFunctionSynonyms[0][$function];
-                                       } else {
-                                               $function = false;
-                                       }
-                               }
-                               if ( $function ) {
-                                       list( $callback, $flags ) = $this->mFunctionHooks[$function];
-                                       $initialArgs = array( &$this );
-                                       $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
-                                       if ( $flags & SFH_OBJECT_ARGS ) {
-                                               # Add a frame parameter, and pass the arguments as an array
-                                               $allArgs = $initialArgs;
-                                               $allArgs[] = $frame;
-                                               for ( $i = 0; $i < $args->getLength(); $i++ ) {
-                                                       $funcArgs[] = $args->item( $i );
-                                               }
-                                               $allArgs[] = $funcArgs;
-                                       } else {
-                                               # Convert arguments to plain text
-                                               for ( $i = 0; $i < $args->getLength(); $i++ ) {
-                                                       $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
-                                               }
-                                               $allArgs = array_merge( $initialArgs, $funcArgs );
-                                       }
-
-                                       # Workaround for PHP bug 35229 and similar
-                                       if ( !is_callable( $callback ) ) {
-                                               throw new MWException( "Tag hook for $name is not callable\n" );
-                                       }
-                                       $result = call_user_func_array( $callback, $allArgs );
-                                       $found = true;
-                                       $noparse = true;
-                                       $preprocessFlags = 0;
-                                       
-                                       if ( is_array( $result ) ) {
-                                               if ( isset( $result[0] ) ) {
-                                                       $text = $result[0];
-                                                       unset( $result[0] );
-                                               }
-
-                                               // Extract flags into the local scope
-                                               // This allows callers to set flags such as nowiki, found, etc.
-                                               extract( $result );
-                                       } else {
-                                               $text = $result;
-                                       }
-                                       if ( !$noparse ) {
-                                               $text = $this->preprocessToDom( $text, $preprocessFlags );
-                                               $isChildObj = true;
-                                       }
-                               }
-                       }
-                       wfProfileOut( __METHOD__ . '-pfunc' );
-               }
-
-               # Finish mangling title and then check for loops.
-               # Set $title to a Title object and $titleText to the PDBK
-               if ( !$found ) {
-                       $ns = NS_TEMPLATE;
-                       # Split the title into page and subpage
-                       $subpage = '';
-                       $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
-                       if ($subpage !== '') {
-                               $ns = $this->mTitle->getNamespace();
-                       }
-                       $title = Title::newFromText( $part1, $ns );
-                       if ( $title ) {
-                               $titleText = $title->getPrefixedText();
-                               # Check for language variants if the template is not found
-                               if($wgContLang->hasVariants() && $title->getArticleID() == 0){
-                                       $wgContLang->findVariantLink($part1, $title);
-                               }
-                               # Do infinite loop check
-                               if ( !$frame->loopCheck( $title ) ) {
-                                       $found = true;
-                                       $text = "<span class=\"error\">Template loop detected: [[$titleText]]</span>";
-                                       wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
-                               }
-                               # Do recursion depth check
-                               $limit = $this->mOptions->getMaxTemplateDepth();
-                               if ( $frame->depth >= $limit ) {
-                                       $found = true;
-                                       $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>";
-                               }
-                       }
-               }
-
-               # Load from database
-               if ( !$found && $title ) {
-                       wfProfileIn( __METHOD__ . '-loadtpl' );
-                       if ( !$title->isExternal() ) {
-                               if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
-                                       $text = SpecialPage::capturePath( $title );
-                                       if ( is_string( $text ) ) {
-                                               $found = true;
-                                               $isHTML = true;
-                                               $this->disableCache();
-                                       }
-                               } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
-                                       $found = false; //access denied
-                                       wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
-                               } else {
-                                       list( $text, $title ) = $this->getTemplateDom( $title );
-                                       if ( $text !== false ) {
-                                               $found = true;
-                                               $isChildObj = true;
-                                       }
-                               }
-
-                               # If the title is valid but undisplayable, make a link to it
-                               if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
-                                       $text = "[[:$titleText]]";
-                                       $found = true;
-                               }
-                       } elseif ( $title->isTrans() ) {
-                               // Interwiki transclusion
-                               if ( $this->ot['html'] && !$forceRawInterwiki ) {
-                                       $text = $this->interwikiTransclude( $title, 'render' );
-                                       $isHTML = true;
-                               } else {
-                                       $text = $this->interwikiTransclude( $title, 'raw' );
-                                       // Preprocess it like a template
-                                       $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
-                                       $isChildObj = true;
-                               }
-                               $found = true;
-                       }
-                       wfProfileOut( __METHOD__ . '-loadtpl' );
-               }
-
-               # If we haven't found text to substitute by now, we're done
-               # Recover the source wikitext and return it
-               if ( !$found ) {
-                       $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
-                       wfProfileOut( $fname );
-                       return array( 'object' => $text );
-               }
-
-               # Expand DOM-style return values in a child frame
-               if ( $isChildObj ) {
-                       # Clean up argument array
-                       $newFrame = $frame->newChild( $args, $title );
-
-                       if ( $nowiki ) {
-                               $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
-                       } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
-                               # Expansion is eligible for the empty-frame cache
-                               if ( isset( $this->mTplExpandCache[$titleText] ) ) {
-                                       $text = $this->mTplExpandCache[$titleText];
-                               } else {
-                                       $text = $newFrame->expand( $text );
-                                       $this->mTplExpandCache[$titleText] = $text;
-                               }
-                       } else {
-                               # Uncached expansion
-                               $text = $newFrame->expand( $text );
-                       }
-               }
-               if ( $isLocalObj && $nowiki ) {
-                       $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
-                       $isLocalObj = false;
-               }
-
-               # Replace raw HTML by a placeholder
-               # Add a blank line preceding, to prevent it from mucking up
-               # immediately preceding headings
-               if ( $isHTML ) {
-                       $text = "\n\n" . $this->insertStripItem( $text );
-               }
-               # Escape nowiki-style return values
-               elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
-                       $text = wfEscapeWikiText( $text );
-               }
-               # Bug 529: if the template begins with a table or block-level
-               # element, it should be treated as beginning a new line.
-               # This behaviour is somewhat controversial.
-               elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
-                       $text = "\n" . $text;
-               }
-
-               if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
-                       # Error, oversize inclusion
-                       $text = "[[$originalTitle]]" .
-                               $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
-                       $this->limitationWarn( 'post-expand-template-inclusion' );
-               }
-
-               if ( $isLocalObj ) {
-                       $ret = array( 'object' => $text );
-               } else {
-                       $ret = array( 'text' => $text );
-               }
-
-               wfProfileOut( $fname );
-               return $ret;
-       }
-
-       /**
-        * Get the semi-parsed DOM representation of a template with a given title,
-        * and its redirect destination title. Cached.
-        */
-       function getTemplateDom( $title ) {
-               $cacheTitle = $title;
-               $titleText = $title->getPrefixedDBkey();
-
-               if ( isset( $this->mTplRedirCache[$titleText] ) ) {
-                       list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
-                       $title = Title::makeTitle( $ns, $dbk );
-                       $titleText = $title->getPrefixedDBkey();
-               }
-               if ( isset( $this->mTplDomCache[$titleText] ) ) {
-                       return array( $this->mTplDomCache[$titleText], $title );
-               }
-
-               // Cache miss, go to the database
-               list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
-
-               if ( $text === false ) {
-                       $this->mTplDomCache[$titleText] = false;
-                       return array( false, $title );
-               }
-
-               $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
-               $this->mTplDomCache[ $titleText ] = $dom;
-
-               if (! $title->equals($cacheTitle)) {
-                       $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
-                               array( $title->getNamespace(),$cdb = $title->getDBkey() );
-               }
-
-               return array( $dom, $title );
-       }
-
-       /**
-        * Fetch the unparsed text of a template and register a reference to it.
-        */
-       function fetchTemplateAndTitle( $title ) {
-               $templateCb = $this->mOptions->getTemplateCallback();
-               $stuff = call_user_func( $templateCb, $title, $this );
-               $text = $stuff['text'];
-               $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
-               if ( isset( $stuff['deps'] ) ) {
-                       foreach ( $stuff['deps'] as $dep ) {
-                               $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
-                       }
-               }
-               return array($text,$finalTitle);
-       }
-
-       function fetchTemplate( $title ) {
-               $rv = $this->fetchTemplateAndTitle($title);
-               return $rv[0];
-       }
-
-       /**
-        * Static function to get a template
-        * Can be overridden via ParserOptions::setTemplateCallback().
-        */
-       static function statelessFetchTemplate( $title, $parser=false ) {
-               $text = $skip = false;
-               $finalTitle = $title;
-               $deps = array();
-
-               // Loop to fetch the article, with up to 1 redirect
-               for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
-                       # Give extensions a chance to select the revision instead
-                       $id = false; // Assume current
-                       wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
-
-                       if( $skip ) {
-                               $text = false;
-                               $deps[] = array(
-                                       'title' => $title,
-                                       'page_id' => $title->getArticleID(),
-                                       'rev_id' => null );
-                               break;
-                       }
-                       $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
-                       $rev_id = $rev ? $rev->getId() : 0;
-                       // If there is no current revision, there is no page
-                       if( $id === false && !$rev ) {
-                               $linkCache = LinkCache::singleton();
-                               $linkCache->addBadLinkObj( $title );
-                       }
-
-                       $deps[] = array(
-                               'title' => $title,
-                               'page_id' => $title->getArticleID(),
-                               'rev_id' => $rev_id );
-
-                       if( $rev ) {
-                               $text = $rev->getText();
-                       } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
-                               global $wgLang;
-                               $message = $wgLang->lcfirst( $title->getText() );
-                               $text = wfMsgForContentNoTrans( $message );
-                               if( wfEmptyMsg( $message, $text ) ) {
-                                       $text = false;
-                                       break;
-                               }
-                       } else {
-                               break;
-                       }
-                       if ( $text === false ) {
-                               break;
-                       }
-                       // Redirect?
-                       $finalTitle = $title;
-                       $title = Title::newFromRedirect( $text );
-               }
-               return array(
-                       'text' => $text,
-                       'finalTitle' => $finalTitle,
-                       'deps' => $deps );
-       }
-
-       /**
-        * Transclude an interwiki link.
-        */
-       function interwikiTransclude( $title, $action ) {
-               global $wgEnableScaryTranscluding;
-
-               if (!$wgEnableScaryTranscluding)
-                       return wfMsg('scarytranscludedisabled');
-
-               $url = $title->getFullUrl( "action=$action" );
-
-               if (strlen($url) > 255)
-                       return wfMsg('scarytranscludetoolong');
-               return $this->fetchScaryTemplateMaybeFromCache($url);
-       }
-
-       function fetchScaryTemplateMaybeFromCache($url) {
-               global $wgTranscludeCacheExpiry;
-               $dbr = wfGetDB(DB_SLAVE);
-               $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
-                               array('tc_url' => $url));
-               if ($obj) {
-                       $time = $obj->tc_time;
-                       $text = $obj->tc_contents;
-                       if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
-                               return $text;
-                       }
-               }
-
-               $text = Http::get($url);
-               if (!$text)
-                       return wfMsg('scarytranscludefailed', $url);
-
-               $dbw = wfGetDB(DB_MASTER);
-               $dbw->replace('transcache', array('tc_url'), array(
-                       'tc_url' => $url,
-                       'tc_time' => time(),
-                       'tc_contents' => $text));
-               return $text;
-       }
-
-
-       /**
-        * Triple brace replacement -- used for template arguments
-        * @private
-        */
-       function argSubstitution( $piece, $frame ) {
-               wfProfileIn( __METHOD__ );
-
-               $error = false;
-               $parts = $piece['parts'];
-               $nameWithSpaces = $frame->expand( $piece['title'] );
-               $argName = trim( $nameWithSpaces );
-               $object = false;
-               $text = $frame->getArgument( $argName );
-               if (  $text === false && $parts->getLength() > 0
-                 && (
-                   $this->ot['html']
-                   || $this->ot['pre']
-                   || ( $this->ot['wiki'] && $frame->isTemplate() )
-                 )
-               ) {
-                       # No match in frame, use the supplied default
-                       $object = $parts->item( 0 )->getChildren();
-               }
-               if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
-                       $error = '<!-- WARNING: argument omitted, expansion size too large -->';
-                       $this->limitationWarn( 'post-expand-template-argument' );
-               }
-
-               if ( $text === false && $object === false ) {
-                       # No match anywhere
-                       $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
-               }
-               if ( $error !== false ) {
-                       $text .= $error;
-               }
-               if ( $object !== false ) {
-                       $ret = array( 'object' => $object );
-               } else {
-                       $ret = array( 'text' => $text );
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $ret;
-       }
-
-       /**
-        * Return the text to be used for a given extension tag.
-        * This is the ghost of strip().
-        *
-        * @param array $params Associative array of parameters:
-        *     name       PPNode for the tag name
-        *     attr       PPNode for unparsed text where tag attributes are thought to be
-        *     attributes Optional associative array of parsed attributes
-        *     inner      Contents of extension element
-        *     noClose    Original text did not have a close tag
-        * @param PPFrame $frame
-        */
-       function extensionSubstitution( $params, $frame ) {
-               global $wgRawHtml, $wgContLang;
-
-               $name = $frame->expand( $params['name'] );
-               $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
-               $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
-
-               $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
-
-               if ( $this->ot['html'] ) {
-                       $name = strtolower( $name );
-
-                       $attributes = Sanitizer::decodeTagAttributes( $attrText );
-                       if ( isset( $params['attributes'] ) ) {
-                               $attributes = $attributes + $params['attributes'];
-                       }
-                       switch ( $name ) {
-                               case 'html':
-                                       if( $wgRawHtml ) {
-                                               $output = $content;
-                                               break;
-                                       } else {
-                                               throw new MWException( '<html> extension tag encountered unexpectedly' );
-                                       }
-                               case 'nowiki':
-                                       $output = Xml::escapeTagsOnly( $content );
-                                       break;
-                               case 'math':
-                                       $output = $wgContLang->armourMath(
-                                               MathRenderer::renderMath( $content, $attributes ) );
-                                       break;
-                               case 'gallery':
-                                       $output = $this->renderImageGallery( $content, $attributes );
-                                       break;
-                               default:
-                                       if( isset( $this->mTagHooks[$name] ) ) {
-                                               # Workaround for PHP bug 35229 and similar
-                                               if ( !is_callable( $this->mTagHooks[$name] ) ) {
-                                                       throw new MWException( "Tag hook for $name is not callable\n" );
-                                               }
-                                               $output = call_user_func_array( $this->mTagHooks[$name],
-                                                       array( $content, $attributes, $this ) );
-                                       } else {
-                                               $output = '<span class="error">Invalid tag extension name: ' .
-                                                       htmlspecialchars( $name ) . '</span>';
-                                       }
-                       }
-               } else {
-                       if ( is_null( $attrText ) ) {
-                               $attrText = '';
-                       }
-                       if ( isset( $params['attributes'] ) ) {
-                               foreach ( $params['attributes'] as $attrName => $attrValue ) {
-                                       $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
-                                               htmlspecialchars( $attrValue ) . '"';
-                               }
-                       }
-                       if ( $content === null ) {
-                               $output = "<$name$attrText/>";
-                       } else {
-                               $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
-                               $output = "<$name$attrText>$content$close";
-                       }
-               }
-
-               if ( $name == 'html' || $name == 'nowiki' ) {
-                       $this->mStripState->nowiki->setPair( $marker, $output );
-               } else {
-                       $this->mStripState->general->setPair( $marker, $output );
-               }
-               return $marker;
-       }
-
-       /**
-        * Increment an include size counter
-        *
-        * @param string $type The type of expansion
-        * @param integer $size The size of the text
-        * @return boolean False if this inclusion would take it over the maximum, true otherwise
-        */
-       function incrementIncludeSize( $type, $size ) {
-               if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) {
-                       return false;
-               } else {
-                       $this->mIncludeSizes[$type] += $size;
-                       return true;
-               }
-       }
-
-       /**
-        * Increment the expensive function count
-        *
-        * @return boolean False if the limit has been exceeded
-        */
-       function incrementExpensiveFunctionCount() {
-               global $wgExpensiveParserFunctionLimit;
-               $this->mExpensiveFunctionCount++;
-               if($this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit) {
-                       return true;
-               }
-               return false;
-       }
-
-       /**
-        * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
-        * Fills $this->mDoubleUnderscores, returns the modified text
-        */
-       function doDoubleUnderscore( $text ) {
-               // The position of __TOC__ needs to be recorded
-               $mw = MagicWord::get( 'toc' );
-               if( $mw->match( $text ) ) {
-                       $this->mShowToc = true;
-                       $this->mForceTocPosition = true;
-
-                       // Set a placeholder. At the end we'll fill it in with the TOC.
-                       $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
-
-                       // Only keep the first one.
-                       $text = $mw->replace( '', $text );
-               }
-
-               // Now match and remove the rest of them
-               $mwa = MagicWord::getDoubleUnderscoreArray();
-               $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
-
-               if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
-                       $this->mOutput->mNoGallery = true;
-               }
-               if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
-                       $this->mShowToc = false;
-               }
-               if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
-                       $this->mOutput->setProperty( 'hiddencat', 'y' );
-
-                       $containerCategory = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( 'hidden-category-category' ) );
-                       if ( $containerCategory ) {
-                               $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
-                       } else {
-                               wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" );
-                       }
-               }
-               return $text;
-       }
-
-       /**
-        * This function accomplishes several tasks:
-        * 1) Auto-number headings if that option is enabled
-        * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
-        * 3) Add a Table of contents on the top for users who have enabled the option
-        * 4) Auto-anchor headings
-        *
-        * It loops through all headlines, collects the necessary data, then splits up the
-        * string and re-inserts the newly formatted headlines.
-        *
-        * @param string $text
-        * @param boolean $isMain
-        * @private
-        */
-       function formatHeadings( $text, $isMain=true ) {
-               global $wgMaxTocLevel, $wgContLang;
-
-               $doNumberHeadings = $this->mOptions->getNumberHeadings();
-               if( !$this->mTitle->quickUserCan( 'edit' ) ) {
-                       $showEditLink = 0;
-               } else {
-                       $showEditLink = $this->mOptions->getEditSection();
-               }
-
-               # Inhibit editsection links if requested in the page
-               if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
-                       $showEditLink = 0;
-               }
-
-               # Get all headlines for numbering them and adding funky stuff like [edit]
-               # links - this is for later, but we need the number of headlines right now
-               $matches = array();
-               $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
-
-               # if there are fewer than 4 headlines in the article, do not show TOC
-               # unless it's been explicitly enabled.
-               $enoughToc = $this->mShowToc &&
-                       (($numMatches >= 4) || $this->mForceTocPosition);
-
-               # Allow user to stipulate that a page should have a "new section"
-               # link added via __NEWSECTIONLINK__
-               if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
-                       $this->mOutput->setNewSection( true );
-               }
-
-               # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
-               # override above conditions and always show TOC above first header
-               if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
-                       $this->mShowToc = true;
-                       $enoughToc = true;
-               }
-
-               # We need this to perform operations on the HTML
-               $sk = $this->mOptions->getSkin();
-
-               # headline counter
-               $headlineCount = 0;
-               $numVisible = 0;
-
-               # Ugh .. the TOC should have neat indentation levels which can be
-               # passed to the skin functions. These are determined here
-               $toc = '';
-               $full = '';
-               $head = array();
-               $sublevelCount = array();
-               $levelCount = array();
-               $toclevel = 0;
-               $level = 0;
-               $prevlevel = 0;
-               $toclevel = 0;
-               $prevtoclevel = 0;
-               $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
-               $baseTitleText = $this->mTitle->getPrefixedDBkey();
-               $tocraw = array();
-
-               foreach( $matches[3] as $headline ) {
-                       $isTemplate = false;
-                       $titleText = false;
-                       $sectionIndex = false;
-                       $numbering = '';
-                       $markerMatches = array();
-                       if (preg_match("/^$markerRegex/", $headline, $markerMatches)) {
-                               $serial = $markerMatches[1];
-                               list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
-                               $isTemplate = ($titleText != $baseTitleText);
-                               $headline = preg_replace("/^$markerRegex/", "", $headline);
-                       }
-
-                       if( $toclevel ) {
-                               $prevlevel = $level;
-                               $prevtoclevel = $toclevel;
-                       }
-                       $level = $matches[1][$headlineCount];
-
-                       if( $doNumberHeadings || $enoughToc ) {
-
-                               if ( $level > $prevlevel ) {
-                                       # Increase TOC level
-                                       $toclevel++;
-                                       $sublevelCount[$toclevel] = 0;
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               $prevtoclevel = $toclevel;
-                                               $toc .= $sk->tocIndent();
-                                               $numVisible++;
-                                       }
-                               }
-                               elseif ( $level < $prevlevel && $toclevel > 1 ) {
-                                       # Decrease TOC level, find level to jump to
-
-                                       if ( $toclevel == 2 && $level <= $levelCount[1] ) {
-                                               # Can only go down to level 1
-                                               $toclevel = 1;
-                                       } else {
-                                               for ($i = $toclevel; $i > 0; $i--) {
-                                                       if ( $levelCount[$i] == $level ) {
-                                                               # Found last matching level
-                                                               $toclevel = $i;
-                                                               break;
-                                                       }
-                                                       elseif ( $levelCount[$i] < $level ) {
-                                                               # Found first matching level below current level
-                                                               $toclevel = $i + 1;
-                                                               break;
-                                                       }
-                                               }
-                                       }
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               if($prevtoclevel < $wgMaxTocLevel) {
-                                                       # Unindent only if the previous toc level was shown :p
-                                                       $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
-                                                       $prevtoclevel = $toclevel;
-                                               } else {
-                                                       $toc .= $sk->tocLineEnd();
-                                               }
-                                       }
-                               }
-                               else {
-                                       # No change in level, end TOC line
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               $toc .= $sk->tocLineEnd();
-                                       }
-                               }
-
-                               $levelCount[$toclevel] = $level;
-
-                               # count number of headlines for each level
-                               @$sublevelCount[$toclevel]++;
-                               $dot = 0;
-                               for( $i = 1; $i <= $toclevel; $i++ ) {
-                                       if( !empty( $sublevelCount[$i] ) ) {
-                                               if( $dot ) {
-                                                       $numbering .= '.';
-                                               }
-                                               $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
-                                               $dot = 1;
-                                       }
-                               }
-                       }
-
-                       # The safe header is a version of the header text safe to use for links
-                       # Avoid insertion of weird stuff like <math> by expanding the relevant sections
-                       $safeHeadline = $this->mStripState->unstripBoth( $headline );
-
-                       # Remove link placeholders by the link text.
-                       #     <!--LINK number-->
-                       # turns into
-                       #     link text with suffix
-                       $safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
-                                                           "\$this->mLinkHolders['texts'][\$1]",
-                                                           $safeHeadline );
-                       $safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
-                                                           "\$this->mInterwikiLinkHolders['texts'][\$1]",
-                                                           $safeHeadline );
-
-                       # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
-                       $tocline = preg_replace(
-                               array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
-                               array( '',                          '<$1>'),
-                               $safeHeadline
-                       );
-                       $tocline = trim( $tocline );
-
-                       # For the anchor, strip out HTML-y stuff period
-                       $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
-                       $safeHeadline = trim( $safeHeadline );
-
-                       # Save headline for section edit hint before it's escaped
-                       $headlineHint = $safeHeadline;
-                       $safeHeadline = Sanitizer::escapeId( $safeHeadline );
-                       # HTML names must be case-insensitively unique (bug 10721)
-                       $arrayKey = strtolower( $safeHeadline );
-
-                       # count how many in assoc. array so we can track dupes in anchors
-                       isset( $refers[$arrayKey] ) ? $refers[$arrayKey]++ : $refers[$arrayKey] = 1;
-                       $refcount[$headlineCount] = $refers[$arrayKey];
-
-                       # Don't number the heading if it is the only one (looks silly)
-                       if( $doNumberHeadings && count( $matches[3] ) > 1) {
-                               # the two are different if the line contains a link
-                               $headline=$numbering . ' ' . $headline;
-                       }
-
-                       # Create the anchor for linking from the TOC to the section
-                       $anchor = $safeHeadline;
-                       if($refcount[$headlineCount] > 1 ) {
-                               $anchor .= '_' . $refcount[$headlineCount];
-                       }
-                       if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
-                               $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
-                               $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
-                       }
-                       # give headline the correct <h#> tag
-                       if( $showEditLink && $sectionIndex !== false ) {
-                               if( $isTemplate ) {
-                                       # Put a T flag in the section identifier, to indicate to extractSections()
-                                       # that sections inside <includeonly> should be counted.
-                                       $editlink = $sk->editSectionLinkForOther($titleText, "T-$sectionIndex");
-                               } else {
-                                       $editlink = $sk->editSectionLink($this->mTitle, $sectionIndex, $headlineHint);
-                               }
-                       } else {
-                               $editlink = '';
-                       }
-                       $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
-
-                       $headlineCount++;
-               }
-
-               $this->mOutput->setSections( $tocraw );
-
-               # Never ever show TOC if no headers
-               if( $numVisible < 1 ) {
-                       $enoughToc = false;
-               }
-
-               if( $enoughToc ) {
-                       if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
-                               $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
-                       }
-                       $toc = $sk->tocList( $toc );
-               }
-
-               # split up and insert constructed headlines
-
-               $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
-               $i = 0;
-
-               foreach( $blocks as $block ) {
-                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
-                               # This is the [edit] link that appears for the top block of text when
-                               # section editing is enabled
-
-                               # Disabled because it broke block formatting
-                               # For example, a bullet point in the top line
-                               # $full .= $sk->editSectionLink(0);
-                       }
-                       $full .= $block;
-                       if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
-                               # Top anchor now in skin
-                               $full = $full.$toc;
-                       }
-
-                       if( !empty( $head[$i] ) ) {
-                               $full .= $head[$i];
-                       }
-                       $i++;
-               }
-               if( $this->mForceTocPosition ) {
-                       return str_replace( '<!--MWTOC-->', $toc, $full );
-               } else {
-                       return $full;
-               }
-       }
-
-       /**
-        * Transform wiki markup when saving a page by doing \r\n -> \n
-        * conversion, substitting signatures, {{subst:}} templates, etc.
-        *
-        * @param string $text the text to transform
-        * @param Title &$title the Title object for the current article
-        * @param User &$user the User object describing the current user
-        * @param ParserOptions $options parsing options
-        * @param bool $clearState whether to clear the parser state first
-        * @return string the altered wiki markup
-        * @public
-        */
-       function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
-               $this->mOptions = $options;
-               $this->setTitle( $title );
-               $this->setOutputType( self::OT_WIKI );
-
-               if ( $clearState ) {
-                       $this->clearState();
-               }
-
-               $pairs = array(
-                       "\r\n" => "\n",
-               );
-               $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
-               $text = $this->pstPass2( $text, $user );
-               $text = $this->mStripState->unstripBoth( $text );
-               return $text;
-       }
-
-       /**
-        * Pre-save transform helper function
-        * @private
-        */
-       function pstPass2( $text, $user ) {
-               global $wgContLang, $wgLocaltimezone;
-
-               /* Note: This is the timestamp saved as hardcoded wikitext to
-                * the database, we use $wgContLang here in order to give
-                * everyone the same signature and use the default one rather
-                * than the one selected in each user's preferences.
-                *
-                * (see also bug 12815)
-                */
-               $ts = $this->mOptions->getTimestamp();
-               $tz = 'UTC';
-               if ( isset( $wgLocaltimezone ) ) {
-                       $unixts = wfTimestamp( TS_UNIX, $ts );
-                       $oldtz = getenv( 'TZ' );
-                       putenv( 'TZ='.$wgLocaltimezone );
-                       $ts = date( 'YmdHis', $unixts );
-                       $tz = date( 'T', $unixts );  # might vary on DST changeover!
-                       putenv( 'TZ='.$oldtz );
-               }
-               $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)";
-
-               # Variable replacement
-               # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
-               $text = $this->replaceVariables( $text );
-
-               # Signatures
-               $sigText = $this->getUserSig( $user );
-               $text = strtr( $text, array(
-                       '~~~~~' => $d,
-                       '~~~~' => "$sigText $d",
-                       '~~~' => $sigText
-               ) );
-
-               # Context links: [[|name]] and [[name (context)|]]
-               #
-               global $wgLegalTitleChars;
-               $tc = "[$wgLegalTitleChars]";
-               $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
-
-               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/";            # [[ns:page (context)|]]
-               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/";  # [[ns:page (context), context|]]
-               $p2 = "/\[\[\\|($tc+)]]/";                                      # [[|page]]
-
-               # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
-               $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
-               $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
-
-               $t = $this->mTitle->getText();
-               $m = array();
-               if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
-                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
-               } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
-                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
-               } else {
-                       # if there's no context, don't bother duplicating the title
-                       $text = preg_replace( $p2, '[[\\1]]', $text );
-               }
-
-               # Trim trailing whitespace
-               $text = rtrim( $text );
-
-               return $text;
-       }
-
-       /**
-        * Fetch the user's signature text, if any, and normalize to
-        * validated, ready-to-insert wikitext.
-        *
-        * @param User $user
-        * @return string
-        * @private
-        */
-       function getUserSig( &$user ) {
-               global $wgMaxSigChars;
-
-               $username = $user->getName();
-               $nickname = $user->getOption( 'nickname' );
-               $nickname = $nickname === '' ? $username : $nickname;
-
-               if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
-                       $nickname = $username;
-                       wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
-               } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
-                       # Sig. might contain markup; validate this
-                       if( $this->validateSig( $nickname ) !== false ) {
-                               # Validated; clean up (if needed) and return it
-                               return $this->cleanSig( $nickname, true );
-                       } else {
-                               # Failed to validate; fall back to the default
-                               $nickname = $username;
-                               wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
-                       }
-               }
-
-               // Make sure nickname doesnt get a sig in a sig
-               $nickname = $this->cleanSigInSig( $nickname );
-
-               # If we're still here, make it a link to the user page
-               $userText = wfEscapeWikiText( $username );
-               $nickText = wfEscapeWikiText( $nickname );
-               if ( $user->isAnon() )  {
-                       return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
-               } else {
-                       return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
-               }
-       }
-
-       /**
-        * Check that the user's signature contains no bad XML
-        *
-        * @param string $text
-        * @return mixed An expanded string, or false if invalid.
-        */
-       function validateSig( $text ) {
-               return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
-       }
-
-       /**
-        * Clean up signature text
-        *
-        * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
-        * 2) Substitute all transclusions
-        *
-        * @param string $text
-        * @param $parsing Whether we're cleaning (preferences save) or parsing
-        * @return string Signature text
-        */
-       function cleanSig( $text, $parsing = false ) {
-               if ( !$parsing ) {
-                       global $wgTitle;
-                       $this->clearState();
-                       $this->setTitle( $wgTitle );
-                       $this->mOptions = new ParserOptions;
-                       $this->setOutputType = self::OT_PREPROCESS;
-               }
-
-               # FIXME: regex doesn't respect extension tags or nowiki
-               #  => Move this logic to braceSubstitution()
-               $substWord = MagicWord::get( 'subst' );
-               $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
-               $substText = '{{' . $substWord->getSynonym( 0 );
-
-               $text = preg_replace( $substRegex, $substText, $text );
-               $text = $this->cleanSigInSig( $text );
-               $dom = $this->preprocessToDom( $text );
-               $frame = $this->getPreprocessor()->newFrame();
-               $text = $frame->expand( $dom );
-
-               if ( !$parsing ) {
-                       $text = $this->mStripState->unstripBoth( $text );
-               }
-
-               return $text;
-       }
-
-       /**
-        * Strip ~~~, ~~~~ and ~~~~~ out of signatures
-        * @param string $text
-        * @return string Signature text with /~{3,5}/ removed
-        */
-       function cleanSigInSig( $text ) {
-               $text = preg_replace( '/~{3,5}/', '', $text );
-               return $text;
-       }
-
-       /**
-        * Set up some variables which are usually set up in parse()
-        * so that an external function can call some class members with confidence
-        * @public
-        */
-       function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
-               $this->setTitle( $title );
-               $this->mOptions = $options;
-               $this->setOutputType( $outputType );
-               if ( $clearState ) {
-                       $this->clearState();
-               }
-       }
-
-       /**
-        * Wrapper for preprocess()
-        *
-        * @param string $text the text to preprocess
-        * @param ParserOptions $options  options
-        * @return string
-        * @public
-        */
-       function transformMsg( $text, $options ) {
-               global $wgTitle;
-               static $executing = false;
-
-               $fname = "Parser::transformMsg";
-
-               # Guard against infinite recursion
-               if ( $executing ) {
-                       return $text;
-               }
-               $executing = true;
-
-               wfProfileIn($fname);
-               $text = $this->preprocess( $text, $wgTitle, $options );
-
-               $executing = false;
-               wfProfileOut($fname);
-               return $text;
-       }
-
-       /**
-        * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
-        * The callback should have the following form:
-        *    function myParserHook( $text, $params, &$parser ) { ... }
-        *
-        * Transform and return $text. Use $parser for any required context, e.g. use
-        * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
-        *
-        * @public
-        *
-        * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
-        * @param mixed $callback The callback function (and object) to use for the tag
-        *
-        * @return The old value of the mTagHooks array associated with the hook
-        */
-       function setHook( $tag, $callback ) {
-               $tag = strtolower( $tag );
-               $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
-               $this->mTagHooks[$tag] = $callback;
-               if( !in_array( $tag, $this->mStripList ) ) {
-                       $this->mStripList[] = $tag;
-               }
-
-               return $oldVal;
-       }
-
-       function setTransparentTagHook( $tag, $callback ) {
-               $tag = strtolower( $tag );
-               $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
-               $this->mTransparentTagHooks[$tag] = $callback;
-
-               return $oldVal;
-       }
-
-       /**
-        * Remove all tag hooks
-        */
-       function clearTagHooks() {
-               $this->mTagHooks = array();
-               $this->mStripList = $this->mDefaultStripList;
-       }
-
-       /**
-        * Create a function, e.g. {{sum:1|2|3}}
-        * The callback function should have the form:
-        *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
-        *
-        * The callback may either return the text result of the function, or an array with the text
-        * in element 0, and a number of flags in the other elements. The names of the flags are
-        * specified in the keys. Valid flags are:
-        *   found                     The text returned is valid, stop processing the template. This
-        *                             is on by default.
-        *   nowiki                    Wiki markup in the return value should be escaped
-        *   isHTML                    The returned text is HTML, armour it against wikitext transformation
-        *
-        * @public
-        *
-        * @param string $id The magic word ID
-        * @param mixed $callback The callback function (and object) to use
-        * @param integer $flags a combination of the following flags:
-        *                SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
-        *
-        * @return The old callback function for this name, if any
-        */
-       function setFunctionHook( $id, $callback, $flags = 0 ) {
-               $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
-               $this->mFunctionHooks[$id] = array( $callback, $flags );
-
-               # Add to function cache
-               $mw = MagicWord::get( $id );
-               if( !$mw )
-                       throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
-
-               $synonyms = $mw->getSynonyms();
-               $sensitive = intval( $mw->isCaseSensitive() );
-
-               foreach ( $synonyms as $syn ) {
-                       # Case
-                       if ( !$sensitive ) {
-                               $syn = strtolower( $syn );
-                       }
-                       # Add leading hash
-                       if ( !( $flags & SFH_NO_HASH ) ) {
-                               $syn = '#' . $syn;
-                       }
-                       # Remove trailing colon
-                       if ( substr( $syn, -1, 1 ) == ':' ) {
-                               $syn = substr( $syn, 0, -1 );
-                       }
-                       $this->mFunctionSynonyms[$sensitive][$syn] = $id;
-               }
-               return $oldVal;
-       }
-
-       /**
-        * Get all registered function hook identifiers
-        *
-        * @return array
-        */
-       function getFunctionHooks() {
-               return array_keys( $this->mFunctionHooks );
-       }
-
-       /**
-        * Replace <!--LINK--> link placeholders with actual links, in the buffer
-        * Placeholders created in Skin::makeLinkObj()
-        * Returns an array of link CSS classes, indexed by PDBK.
-        * $options is a bit field, RLH_FOR_UPDATE to select for update
-        */
-       function replaceLinkHolders( &$text, $options = 0 ) {
-               global $wgUser;
-               global $wgContLang;
-
-               $fname = 'Parser::replaceLinkHolders';
-               wfProfileIn( $fname );
-
-               $pdbks = array();
-               $colours = array();
-               $linkcolour_ids = array();
-               $sk = $this->mOptions->getSkin();
-               $linkCache = LinkCache::singleton();
-
-               if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
-                       wfProfileIn( $fname.'-check' );
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $page = $dbr->tableName( 'page' );
-                       $threshold = $wgUser->getOption('stubthreshold');
-
-                       # Sort by namespace
-                       asort( $this->mLinkHolders['namespaces'] );
-
-                       # Generate query
-                       $query = false;
-                       $current = null;
-                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                               # Make title object
-                               $title = $this->mLinkHolders['titles'][$key];
-
-                               # Skip invalid entries.
-                               # Result will be ugly, but prevents crash.
-                               if ( is_null( $title ) ) {
-                                       continue;
-                               }
-                               $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
-
-                               # Check if it's a static known link, e.g. interwiki
-                               if ( $title->isAlwaysKnown() ) {
-                                       $colours[$pdbk] = '';
-                               } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-                                       $colours[$pdbk] = '';
-                                       $this->mOutput->addLink( $title, $id );
-                               } elseif ( $linkCache->isBadLink( $pdbk ) ) {
-                                       $colours[$pdbk] = 'new';
-                               } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
-                                       $colours[$pdbk] = 'new';
-                               } else {
-                                       # Not in the link cache, add it to the query
-                                       if ( !isset( $current ) ) {
-                                               $current = $ns;
-                                               $query =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-                                               $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
-                                       } elseif ( $current != $ns ) {
-                                               $current = $ns;
-                                               $query .= ")) OR (page_namespace=$ns AND page_title IN(";
-                                       } else {
-                                               $query .= ', ';
-                                       }
-
-                                       $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
-                               }
-                       }
-                       if ( $query ) {
-                               $query .= '))';
-                               if ( $options & RLH_FOR_UPDATE ) {
-                                       $query .= ' FOR UPDATE';
-                               }
-
-                               $res = $dbr->query( $query, $fname );
-
-                               # Fetch data and form into an associative array
-                               # non-existent = broken
-                               while ( $s = $dbr->fetchObject($res) ) {
-                                       $title = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                       $pdbk = $title->getPrefixedDBkey();
-                                       $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
-                                       $this->mOutput->addLink( $title, $s->page_id );
-                                       $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
-                                       //add id to the extension todolist
-                                       $linkcolour_ids[$s->page_id] = $pdbk;
-                               }
-                               //pass an array of page_ids to an extension
-                               wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
-                       }
-                       wfProfileOut( $fname.'-check' );
-
-                       # Do a second query for different language variants of links and categories
-                       if($wgContLang->hasVariants()){
-                               $linkBatch = new LinkBatch();
-                               $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
-                               $categoryMap = array(); // maps $category_variant => $category (dbkeys)
-                               $varCategories = array(); // category replacements oldDBkey => newDBkey
-
-                               $categories = $this->mOutput->getCategoryLinks();
-
-                               // Add variants of links to link batch
-                               foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                                       $title = $this->mLinkHolders['titles'][$key];
-                                       if ( is_null( $title ) )
-                                               continue;
-
-                                       $pdbk = $title->getPrefixedDBkey();
-                                       $titleText = $title->getText();
-
-                                       // generate all variants of the link title text
-                                       $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
-
-                                       // if link was not found (in first query), add all variants to query
-                                       if ( !isset($colours[$pdbk]) ){
-                                               foreach($allTextVariants as $textVariant){
-                                                       if($textVariant != $titleText){
-                                                               $variantTitle = Title::makeTitle( $ns, $textVariant );
-                                                               if(is_null($variantTitle)) continue;
-                                                               $linkBatch->addObj( $variantTitle );
-                                                               $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               // process categories, check if a category exists in some variant
-                               foreach( $categories as $category ){
-                                       $variants = $wgContLang->convertLinkToAllVariants($category);
-                                       foreach($variants as $variant){
-                                               if($variant != $category){
-                                                       $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
-                                                       if(is_null($variantTitle)) continue;
-                                                       $linkBatch->addObj( $variantTitle );
-                                                       $categoryMap[$variant] = $category;
-                                               }
-                                       }
-                               }
-
-
-                               if(!$linkBatch->isEmpty()){
-                                       // construct query
-                                       $titleClause = $linkBatch->constructSet('page', $dbr);
-
-                                       $variantQuery =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-
-                                       $variantQuery .= " FROM $page WHERE $titleClause";
-                                       if ( $options & RLH_FOR_UPDATE ) {
-                                               $variantQuery .= ' FOR UPDATE';
-                                       }
-
-                                       $varRes = $dbr->query( $variantQuery, $fname );
-
-                                       // for each found variants, figure out link holders and replace
-                                       while ( $s = $dbr->fetchObject($varRes) ) {
-
-                                               $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                               $varPdbk = $variantTitle->getPrefixedDBkey();
-                                               $vardbk = $variantTitle->getDBkey();
-
-                                               $holderKeys = array();
-                                               if(isset($variantMap[$varPdbk])){
-                                                       $holderKeys = $variantMap[$varPdbk];
-                                                       $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
-                                                       $this->mOutput->addLink( $variantTitle, $s->page_id );
-                                               }
-
-                                               // loop over link holders
-                                               foreach($holderKeys as $key){
-                                                       $title = $this->mLinkHolders['titles'][$key];
-                                                       if ( is_null( $title ) ) continue;
-
-                                                       $pdbk = $title->getPrefixedDBkey();
-
-                                                       if(!isset($colours[$pdbk])){
-                                                               // found link in some of the variants, replace the link holder data
-                                                               $this->mLinkHolders['titles'][$key] = $variantTitle;
-                                                               $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
-
-                                                               // set pdbk and colour
-                                                               $pdbks[$key] = $varPdbk;
-                                                               $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
-                                                               $linkcolour_ids[$s->page_id] = $pdbk;
-                                                       }
-                                                       wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
-                                               }
-
-                                               // check if the object is a variant of a category
-                                               if(isset($categoryMap[$vardbk])){
-                                                       $oldkey = $categoryMap[$vardbk];
-                                                       if($oldkey != $vardbk)
-                                                               $varCategories[$oldkey]=$vardbk;
-                                               }
-                                       }
-
-                                       // rebuild the categories in original order (if there are replacements)
-                                       if(count($varCategories)>0){
-                                               $newCats = array();
-                                               $originalCats = $this->mOutput->getCategories();
-                                               foreach($originalCats as $cat => $sortkey){
-                                                       // make the replacement
-                                                       if( array_key_exists($cat,$varCategories) )
-                                                               $newCats[$varCategories[$cat]] = $sortkey;
-                                                       else $newCats[$cat] = $sortkey;
-                                               }
-                                               $this->mOutput->setCategoryLinks($newCats);
-                                       }
-                               }
-                       }
-
-                       # Construct search and replace arrays
-                       wfProfileIn( $fname.'-construct' );
-                       $replacePairs = array();
-                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                               $pdbk = $pdbks[$key];
-                               $searchkey = "<!--LINK $key-->";
-                               $title = $this->mLinkHolders['titles'][$key];
-                               if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
-                                       $linkCache->addBadLinkObj( $title );
-                                       $colours[$pdbk] = 'new';
-                                       $this->mOutput->addLink( $title, 0 );
-                                       $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               } else {
-                                       $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               }
-                       }
-                       $replacer = new HashtableReplacer( $replacePairs, 1 );
-                       wfProfileOut( $fname.'-construct' );
-
-                       # Do the thing
-                       wfProfileIn( $fname.'-replace' );
-                       $text = preg_replace_callback(
-                               '/(<!--LINK .*?-->)/',
-                               $replacer->cb(),
-                               $text);
-
-                       wfProfileOut( $fname.'-replace' );
-               }
-
-               # Now process interwiki link holders
-               # This is quite a bit simpler than internal links
-               if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
-                       wfProfileIn( $fname.'-interwiki' );
-                       # Make interwiki link HTML
-                       $replacePairs = array();
-                       foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
-                               $title = $this->mInterwikiLinkHolders['titles'][$key];
-                               $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
-                       }
-                       $replacer = new HashtableReplacer( $replacePairs, 1 );
-
-                       $text = preg_replace_callback(
-                               '/<!--IWLINK (.*?)-->/',
-                               $replacer->cb(),
-                               $text );
-                       wfProfileOut( $fname.'-interwiki' );
-               }
-
-               wfProfileOut( $fname );
-               return $colours;
-       }
-
-       /**
-        * Replace <!--LINK--> link placeholders with plain text of links
-        * (not HTML-formatted).
-        * @param string $text
-        * @return string
-        */
-       function replaceLinkHoldersText( $text ) {
-               $fname = 'Parser::replaceLinkHoldersText';
-               wfProfileIn( $fname );
-
-               $text = preg_replace_callback(
-                       '/<!--(LINK|IWLINK) (.*?)-->/',
-                       array( &$this, 'replaceLinkHoldersTextCallback' ),
-                       $text );
-
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        * @private
-        */
-       function replaceLinkHoldersTextCallback( $matches ) {
-               $type = $matches[1];
-               $key  = $matches[2];
-               if( $type == 'LINK' ) {
-                       if( isset( $this->mLinkHolders['texts'][$key] ) ) {
-                               return $this->mLinkHolders['texts'][$key];
-                       }
-               } elseif( $type == 'IWLINK' ) {
-                       if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
-                               return $this->mInterwikiLinkHolders['texts'][$key];
-                       }
-               }
-               return $matches[0];
-       }
-
-       /**
-        * Tag hook handler for 'pre'.
-        */
-       function renderPreTag( $text, $attribs ) {
-               // Backwards-compatibility hack
-               $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
-
-               $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
-               return wfOpenElement( 'pre', $attribs ) .
-                       Xml::escapeTagsOnly( $content ) .
-                       '</pre>';
-       }
-
-       /**
-        * Renders an image gallery from a text with one line per image.
-        * text labels may be given by using |-style alternative text. E.g.
-        *   Image:one.jpg|The number "1"
-        *   Image:tree.jpg|A tree
-        * given as text will return the HTML of a gallery with two images,
-        * labeled 'The number "1"' and
-        * 'A tree'.
-        */
-       function renderImageGallery( $text, $params ) {
-               $ig = new ImageGallery();
-               $ig->setContextTitle( $this->mTitle );
-               $ig->setShowBytes( false );
-               $ig->setShowFilename( false );
-               $ig->setParser( $this );
-               $ig->setHideBadImages();
-               $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
-               $ig->useSkin( $this->mOptions->getSkin() );
-               $ig->mRevisionId = $this->mRevisionId;
-
-               if( isset( $params['caption'] ) ) {
-                       $caption = $params['caption'];
-                       $caption = htmlspecialchars( $caption );
-                       $caption = $this->replaceInternalLinks( $caption );
-                       $ig->setCaptionHtml( $caption );
-               }
-               if( isset( $params['perrow'] ) ) {
-                       $ig->setPerRow( $params['perrow'] );
-               }
-               if( isset( $params['widths'] ) ) {
-                       $ig->setWidths( $params['widths'] );
-               }
-               if( isset( $params['heights'] ) ) {
-                       $ig->setHeights( $params['heights'] );
-               }
-
-               wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
-
-               $lines = explode( "\n", $text );
-               foreach ( $lines as $line ) {
-                       # match lines like these:
-                       # Image:someimage.jpg|This is some image
-                       $matches = array();
-                       preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
-                       # Skip empty lines
-                       if ( count( $matches ) == 0 ) {
-                               continue;
-                       }
-                       
-                       if ( strpos( $matches[0], '%' ) !== false )
-                               $matches[1] = urldecode( $matches[1] );
-                       $tp = Title::newFromText( $matches[1] );
-                       $nt =& $tp;
-                       if( is_null( $nt ) ) {
-                               # Bogus title. Ignore these so we don't bomb out later.
-                               continue;
-                       }
-                       if ( isset( $matches[3] ) ) {
-                               $label = $matches[3];
-                       } else {
-                               $label = '';
-                       }
-
-                       $html = $this->recursiveTagParse( trim( $label ) );
-
-                       $ig->add( $nt, $html );
-
-                       # Only add real images (bug #5586)
-                       if ( $nt->getNamespace() == NS_IMAGE ) {
-                               $this->mOutput->addImage( $nt->getDBkey() );
-                       }
-               }
-               return $ig->toHTML();
-       }
-
-       function getImageParams( $handler ) {
-               if ( $handler ) {
-                       $handlerClass = get_class( $handler );
-               } else {
-                       $handlerClass = '';
-               }
-               if ( !isset( $this->mImageParams[$handlerClass]  ) ) {
-                       // Initialise static lists
-                       static $internalParamNames = array(
-                               'horizAlign' => array( 'left', 'right', 'center', 'none' ),
-                               'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
-                                       'bottom', 'text-bottom' ),
-                               'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
-                                       'upright', 'border' ),
-                       );
-                       static $internalParamMap;
-                       if ( !$internalParamMap ) {
-                               $internalParamMap = array();
-                               foreach ( $internalParamNames as $type => $names ) {
-                                       foreach ( $names as $name ) {
-                                               $magicName = str_replace( '-', '_', "img_$name" );
-                                               $internalParamMap[$magicName] = array( $type, $name );
-                                       }
-                               }
-                       }
-
-                       // Add handler params
-                       $paramMap = $internalParamMap;
-                       if ( $handler ) {
-                               $handlerParamMap = $handler->getParamMap();
-                               foreach ( $handlerParamMap as $magic => $paramName ) {
-                                       $paramMap[$magic] = array( 'handler', $paramName );
-                               }
-                       }
-                       $this->mImageParams[$handlerClass] = $paramMap;
-                       $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
-               }
-               return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
-       }
-
-       /**
-        * Parse image options text and use it to make an image
-        */
-       function makeImage( $title, $options ) {
-               # Check if the options text is of the form "options|alt text"
-               # Options are:
-               #  * thumbnail          make a thumbnail with enlarge-icon and caption, alignment depends on lang
-               #  * left               no resizing, just left align. label is used for alt= only
-               #  * right              same, but right aligned
-               #  * none               same, but not aligned
-               #  * ___px              scale to ___ pixels width, no aligning. e.g. use in taxobox
-               #  * center             center the image
-               #  * framed             Keep original image size, no magnify-button.
-               #  * frameless          like 'thumb' but without a frame. Keeps user preferences for width
-               #  * upright            reduce width for upright images, rounded to full __0 px
-               #  * border             draw a 1px border around the image
-               # vertical-align values (no % or length right now):
-               #  * baseline
-               #  * sub
-               #  * super
-               #  * top
-               #  * text-top
-               #  * middle
-               #  * bottom
-               #  * text-bottom
-
-               $parts = array_map( 'trim', explode( '|', $options) );
-               $sk = $this->mOptions->getSkin();
-
-               # Give extensions a chance to select the file revision for us
-               $skip = $time = $descQuery = false;
-               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
-
-               if ( $skip ) {
-                       return $sk->makeLinkObj( $title );
-               }
-
-               # Get parameter map
-               $file = wfFindFile( $title, $time );
-               $handler = $file ? $file->getHandler() : false;
-
-               list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
-
-               # Process the input parameters
-               $caption = '';
-               $params = array( 'frame' => array(), 'handler' => array(),
-                       'horizAlign' => array(), 'vertAlign' => array() );
-               foreach( $parts as $part ) {
-                       list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
-                       $validated = false;
-                       if( isset( $paramMap[$magicName] ) ) {
-                               list( $type, $paramName ) = $paramMap[$magicName];
-
-                               // Special case; width and height come in one variable together
-                               if( $type == 'handler' && $paramName == 'width' ) {
-                                       $m = array();
-                                       # (bug 13500) In both cases (width/height and width only),
-                                       # permit trailing "px" for backward compatibility.
-                                       if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
-                                               $width = intval( $m[1] );
-                                               $height = intval( $m[2] );
-                                               if ( $handler->validateParam( 'width', $width ) ) {
-                                                       $params[$type]['width'] = $width;
-                                                       $validated = true;
-                                               }
-                                               if ( $handler->validateParam( 'height', $height ) ) {
-                                                       $params[$type]['height'] = $height;
-                                                       $validated = true;
-                                               }
-                                       } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
-                                               $width = intval( $value );
-                                               if ( $handler->validateParam( 'width', $width ) ) {
-                                                       $params[$type]['width'] = $width;
-                                                       $validated = true;
-                                               }
-                                       } // else no validation -- bug 13436
-                               } else {
-                                       if ( $type == 'handler' ) {
-                                               # Validate handler parameter
-                                               $validated = $handler->validateParam( $paramName, $value );
-                                       } else {
-                                               # Validate internal parameters
-                                               switch( $paramName ) {
-                                               case "manualthumb":
-                                                       /// @fixme - possibly check validity here?
-                                                       /// downstream behavior seems odd with missing manual thumbs.
-                                                       $validated = true;
-                                                       break;
-                                               default:
-                                                       // Most other things appear to be empty or numeric...
-                                                       $validated = ( $value === false || is_numeric( trim( $value ) ) );
-                                               }
-                                       }
-
-                                       if ( $validated ) {
-                                               $params[$type][$paramName] = $value;
-                                       }
-                               }
-                       }
-                       if ( !$validated ) {
-                               $caption = $part;
-                       }
-               }
-
-               # Process alignment parameters
-               if ( $params['horizAlign'] ) {
-                       $params['frame']['align'] = key( $params['horizAlign'] );
-               }
-               if ( $params['vertAlign'] ) {
-                       $params['frame']['valign'] = key( $params['vertAlign'] );
-               }
-
-               # Strip bad stuff out of the alt text
-               $alt = $this->replaceLinkHoldersText( $caption );
-
-               # make sure there are no placeholders in thumbnail attributes
-               # that are later expanded to html- so expand them now and
-               # remove the tags
-               $alt = $this->mStripState->unstripBoth( $alt );
-               $alt = Sanitizer::stripAllTags( $alt );
-
-               $params['frame']['alt'] = $alt;
-               $params['frame']['caption'] = $caption;
-
-               wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
-
-               # Linker does the rest
-               $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery );
-
-               # Give the handler a chance to modify the parser object
-               if ( $handler ) {
-                       $handler->parserTransformHook( $this, $file );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Set a flag in the output object indicating that the content is dynamic and
-        * shouldn't be cached.
-        */
-       function disableCache() {
-               wfDebug( "Parser output marked as uncacheable.\n" );
-               $this->mOutput->mCacheTime = -1;
-       }
-
-       /**#@+
-        * Callback from the Sanitizer for expanding items found in HTML attribute
-        * values, so they can be safely tested and escaped.
-        * @param string $text
-        * @param PPFrame $frame
-        * @return string
-        * @private
-        */
-       function attributeStripCallback( &$text, $frame = false ) {
-               $text = $this->replaceVariables( $text, $frame );
-               $text = $this->mStripState->unstripBoth( $text );
-               return $text;
-       }
-
-       /**#@-*/
-
-       /**#@+
-        * Accessor/mutator
-        */
-       function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
-       function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
-       function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
-       /**#@-*/
-
-       /**#@+
-        * Accessor
-        */
-       function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
-       /**#@-*/
-
-
-       /**
-        * Break wikitext input into sections, and either pull or replace
-        * some particular section's text.
-        *
-        * External callers should use the getSection and replaceSection methods.
-        *
-        * @param string $text Page wikitext
-        * @param string $section A section identifier string of the form:
-        *   <flag1> - <flag2> - ... - <section number>
-        *
-        * Currently the only recognised flag is "T", which means the target section number
-        * was derived during a template inclusion parse, in other words this is a template
-        * section edit link. If no flags are given, it was an ordinary section edit link.
-        * This flag is required to avoid a section numbering mismatch when a section is
-        * enclosed by <includeonly> (bug 6563).
-        *
-        * The section number 0 pulls the text before the first heading; other numbers will
-        * pull the given section along with its lower-level subsections. If the section is
-        * not found, $mode=get will return $newtext, and $mode=replace will return $text.
-        *
-        * @param string $mode One of "get" or "replace"
-        * @param string $newText Replacement text for section data.
-        * @return string for "get", the extracted section text.
-        *                for "replace", the whole page with the section replaced.
-        */
-       private function extractSections( $text, $section, $mode, $newText='' ) {
-               global $wgTitle;
-               $this->clearState();
-               $this->setTitle( $wgTitle ); // not generally used but removes an ugly failure mode
-               $this->mOptions = new ParserOptions;
-               $this->setOutputType( self::OT_WIKI );
-               $outText = '';
-               $frame = $this->getPreprocessor()->newFrame();
-
-               // Process section extraction flags
-               $flags = 0;
-               $sectionParts = explode( '-', $section );
-               $sectionIndex = array_pop( $sectionParts );
-               foreach ( $sectionParts as $part ) {
-                       if ( $part == 'T' ) {
-                               $flags |= self::PTD_FOR_INCLUSION;
-                       }
-               }
-               // Preprocess the text
-               $root = $this->preprocessToDom( $text, $flags );
-
-               // <h> nodes indicate section breaks
-               // They can only occur at the top level, so we can find them by iterating the root's children
-               $node = $root->getFirstChild();
-
-               // Find the target section
-               if ( $sectionIndex == 0 ) {
-                       // Section zero doesn't nest, level=big
-                       $targetLevel = 1000;
-               } else {
-            while ( $node ) {
-                if ( $node->getName() == 'h' ) {
-                    $bits = $node->splitHeading();
-                                       if ( $bits['i'] == $sectionIndex ) {
-                                       $targetLevel = $bits['level'];
-                                               break;
-                                       }
-                               }
-                               if ( $mode == 'replace' ) {
-                                       $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
-                               }
-                               $node = $node->getNextSibling();
-                       }
-               }
-
-               if ( !$node ) {
-                       // Not found
-                       if ( $mode == 'get' ) {
-                               return $newText;
-                       } else {
-                               return $text;
-                       }
-               }
-
-               // Find the end of the section, including nested sections
-               do {
-                       if ( $node->getName() == 'h' ) {
-                               $bits = $node->splitHeading();
-                               $curLevel = $bits['level'];
-                               if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
-                                       break;
-                               }
-                       }
-                       if ( $mode == 'get' ) {
-                               $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
-                       }
-                       $node = $node->getNextSibling();
-               } while ( $node );
-
-               // Write out the remainder (in replace mode only)
-               if ( $mode == 'replace' ) {
-                       // Output the replacement text
-                       // Add two newlines on -- trailing whitespace in $newText is conventionally
-                       // stripped by the editor, so we need both newlines to restore the paragraph gap
-                       $outText .= $newText . "\n\n";
-                       while ( $node ) {
-                               $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
-                               $node = $node->getNextSibling();
-                       }
-               }
-
-               if ( is_string( $outText ) ) {
-                       // Re-insert stripped tags
-                       $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
-               }
-
-               return $outText;
-       }
-
-       /**
-        * This function returns the text of a section, specified by a number ($section).
-        * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
-        * the first section before any such heading (section 0).
-        *
-        * If a section contains subsections, these are also returned.
-        *
-        * @param string $text text to look in
-        * @param string $section section identifier
-        * @param string $deftext default to return if section is not found
-        * @return string text of the requested section
-        */
-       public function getSection( $text, $section, $deftext='' ) {
-               return $this->extractSections( $text, $section, "get", $deftext );
-       }
-
-       public function replaceSection( $oldtext, $section, $text ) {
-               return $this->extractSections( $oldtext, $section, "replace", $text );
-       }
-
-       /**
-        * Get the timestamp associated with the current revision, adjusted for
-        * the default server-local timestamp
-        */
-       function getRevisionTimestamp() {
-               if ( is_null( $this->mRevisionTimestamp ) ) {
-                       wfProfileIn( __METHOD__ );
-                       global $wgContLang;
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
-                                       array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
-
-                       // Normalize timestamp to internal MW format for timezone processing.
-                       // This has the added side-effect of replacing a null value with
-                       // the current time, which gives us more sensible behavior for
-                       // previews.
-                       $timestamp = wfTimestamp( TS_MW, $timestamp );
-
-                       // The cryptic '' timezone parameter tells to use the site-default
-                       // timezone offset instead of the user settings.
-                       //
-                       // Since this value will be saved into the parser cache, served
-                       // to other users, and potentially even used inside links and such,
-                       // it needs to be consistent for all visitors.
-                       $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
-
-                       wfProfileOut( __METHOD__ );
-               }
-               return $this->mRevisionTimestamp;
-       }
-
-       /**
-        * Mutator for $mDefaultSort
-        *
-        * @param $sort New value
-        */
-       public function setDefaultSort( $sort ) {
-               $this->mDefaultSort = $sort;
-       }
-
-       /**
-        * Accessor for $mDefaultSort
-        * Will use the title/prefixed title if none is set
-        *
-        * @return string
-        */
-       public function getDefaultSort() {
-               if( $this->mDefaultSort !== false ) {
-                       return $this->mDefaultSort;
-               } else {
-                       return $this->mTitle->getNamespace() == NS_CATEGORY
-                                       ? $this->mTitle->getText()
-                                       : $this->mTitle->getPrefixedText();
-               }
-       }
-
-       /**
-        * Try to guess the section anchor name based on a wikitext fragment
-        * presumably extracted from a heading, for example "Header" from
-        * "== Header ==".
-        */
-       public function guessSectionNameFromWikiText( $text ) {
-               # Strip out wikitext links(they break the anchor)
-               $text = $this->stripSectionName( $text );
-               $headline = Sanitizer::decodeCharReferences( $text );
-               # strip out HTML
-               $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
-               $headline = trim( $headline );
-               $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
-               $replacearray = array(
-                       '%3A' => ':',
-                       '%' => '.'
-               );
-               return str_replace(
-                       array_keys( $replacearray ),
-                       array_values( $replacearray ),
-                       $sectionanchor );
-       }
-
-       /**
-        * Strips a text string of wikitext for use in a section anchor
-        *
-        * Accepts a text string and then removes all wikitext from the
-        * string and leaves only the resultant text (i.e. the result of
-        * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
-        * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
-        * to create valid section anchors by mimicing the output of the
-        * parser when headings are parsed.
-        *
-        * @param $text string Text string to be stripped of wikitext
-        * for use in a Section anchor
-        * @return Filtered text string
-        */
-       public function stripSectionName( $text ) {
-               # Strip internal link markup
-               $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
-               $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
-
-               # Strip external link markup (FIXME: Not Tolerant to blank link text
-               # I.E. [http://www.mediawiki.org] will render as [1] or something depending
-               # on how many empty links there are on the page - need to figure that out.
-               $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
-
-               # Parse wikitext quotes (italics & bold)
-               $text = $this->doQuotes($text);
-
-               # Strip HTML tags
-               $text = StringUtils::delimiterReplace( '<', '>', '', $text );
-               return $text;
-       }
-
-       function srvus( $text ) {
-               return $this->testSrvus( $text, $this->mOutputType );
-       }
-
-       /**
-        * strip/replaceVariables/unstrip for preprocessor regression testing
-        */
-       function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
-               $this->clearState();
-               if ( ! ( $title instanceof Title ) ) {
-                       $title = Title::newFromText( $title );
-               }
-               $this->mTitle = $title;
-               $this->mOptions = $options;
-               $this->setOutputType( $outputType );
-               $text = $this->replaceVariables( $text );
-               $text = $this->mStripState->unstripBoth( $text );
-               $text = Sanitizer::removeHTMLtags( $text );
-               return $text;
-       }
-
-       function testPst( $text, $title, $options ) {
-               global $wgUser;
-               if ( ! ( $title instanceof Title ) ) {
-                       $title = Title::newFromText( $title );
-               }
-               return $this->preSaveTransform( $text, $title, $wgUser, $options );
-       }
-
-       function testPreprocess( $text, $title, $options ) {
-               if ( ! ( $title instanceof Title ) ) {
-                       $title = Title::newFromText( $title );
-               }
-               return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
-       }
-
-       function markerSkipCallback( $s, $callback ) {
-               $i = 0;
-               $out = '';
-               while ( $i < strlen( $s ) ) {
-                       $markerStart = strpos( $s, $this->mUniqPrefix, $i );
-                       if ( $markerStart === false ) {
-                               $out .= call_user_func( $callback, substr( $s, $i ) );
-                               break;
-                       } else {
-                               $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
-                               $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
-                               if ( $markerEnd === false ) {
-                                       $out .= substr( $s, $markerStart );
-                                       break;
-                               } else {
-                                       $markerEnd += strlen( self::MARKER_SUFFIX );
-                                       $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
-                                       $i = $markerEnd;
-                               }
-                       }
-               }
-               return $out;
-       }
-}
-
-/**
- * @todo document, briefly.
- * @ingroup Parser
- */
-class StripState {
-       var $general, $nowiki;
-
-       function __construct() {
-               $this->general = new ReplacementArray;
-               $this->nowiki = new ReplacementArray;
-       }
-
-       function unstripGeneral( $text ) {
-               wfProfileIn( __METHOD__ );
-               do {
-                       $oldText = $text;
-                       $text = $this->general->replace( $text );
-               } while ( $text != $oldText );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       function unstripNoWiki( $text ) {
-               wfProfileIn( __METHOD__ );
-               do {
-                       $oldText = $text;
-                       $text = $this->nowiki->replace( $text );
-               } while ( $text != $oldText );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       function unstripBoth( $text ) {
-               wfProfileIn( __METHOD__ );
-               do {
-                       $oldText = $text;
-                       $text = $this->general->replace( $text );
-                       $text = $this->nowiki->replace( $text );
-               } while ( $text != $oldText );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-}
-
-/**
- * @todo document, briefly.
- * @ingroup Parser
- */
-class OnlyIncludeReplacer {
-       var $output = '';
-
-       function replace( $matches ) {
-               if ( substr( $matches[1], -1 ) == "\n" ) {
-                       $this->output .= substr( $matches[1], 0, -1 );
-               } else {
-                       $this->output .= $matches[1];
-               }
-       }
-}
diff --git a/includes/ParserCache.php b/includes/ParserCache.php
deleted file mode 100644 (file)
index bf11da2..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-/**
- * @ingroup Cache Parser
- * @todo document
- */
-class ParserCache {
-       /**
-        * Get an instance of this object
-        */
-       public static function &singleton() {
-               static $instance;
-               if ( !isset( $instance ) ) {
-                       global $parserMemc;
-                       $instance = new ParserCache( $parserMemc );
-               }
-               return $instance;
-       }
-
-       /**
-        * Setup a cache pathway with a given back-end storage mechanism.
-        * May be a memcached client or a BagOStuff derivative.
-        *
-        * @param object $memCached
-        */
-       function __construct( &$memCached ) {
-               $this->mMemc =& $memCached;
-       }
-
-       function getKey( &$article, &$user ) {
-               global $action;
-               $hash = $user->getPageRenderingHash();
-               if( !$article->mTitle->quickUserCan( 'edit' ) ) {
-                       // section edit links are suppressed even if the user has them on
-                       $edit = '!edit=0';
-               } else {
-                       $edit = '';
-               }
-               $pageid = intval( $article->getID() );
-               $renderkey = (int)($action == 'render');
-               $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" );
-               return $key;
-       }
-
-       function getETag( &$article, &$user ) {
-               return 'W/"' . $this->getKey($article, $user) . "--" . $article->mTouched. '"';
-       }
-
-       function get( &$article, &$user ) {
-               global $wgCacheEpoch;
-               $fname = 'ParserCache::get';
-               wfProfileIn( $fname );
-
-               $key = $this->getKey( $article, $user );
-
-               wfDebug( "Trying parser cache $key\n" );
-               $value = $this->mMemc->get( $key );
-               if ( is_object( $value ) ) {
-                       wfDebug( "Found.\n" );
-                       # Delete if article has changed since the cache was made
-                       $canCache = $article->checkTouched();
-                       $cacheTime = $value->getCacheTime();
-                       $touched = $article->mTouched;
-                       if ( !$canCache || $value->expired( $touched ) ) {
-                               if ( !$canCache ) {
-                                       wfIncrStats( "pcache_miss_invalid" );
-                                       wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
-                               } else {
-                                       wfIncrStats( "pcache_miss_expired" );
-                                       wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
-                               }
-                               $this->mMemc->delete( $key );
-                               $value = false;
-                       } else {
-                               if ( isset( $value->mTimestamp ) ) {
-                                       $article->mTimestamp = $value->mTimestamp;
-                               }
-                               wfIncrStats( "pcache_hit" );
-                       }
-               } else {
-                       wfDebug( "Parser cache miss.\n" );
-                       wfIncrStats( "pcache_miss_absent" );
-                       $value = false;
-               }
-
-               wfProfileOut( $fname );
-               return $value;
-       }
-
-       function save( $parserOutput, &$article, &$user ){
-               global $wgParserCacheExpireTime;
-               $key = $this->getKey( $article, $user );
-
-               if( $parserOutput->getCacheTime() != -1 ) {
-
-                       $now = wfTimestampNow();
-                       $parserOutput->setCacheTime( $now );
-
-                       // Save the timestamp so that we don't have to load the revision row on view
-                       $parserOutput->mTimestamp = $article->getTimestamp();
-
-                       $parserOutput->mText .= "\n<!-- Saved in parser cache with key $key and timestamp $now -->\n";
-                       wfDebug( "Saved in parser cache with key $key and timestamp $now\n" );
-
-                       if( $parserOutput->containsOldMagic() ){
-                               $expire = 3600; # 1 hour
-                       } else {
-                               $expire = $wgParserCacheExpireTime;
-                       }
-                       $this->mMemc->set( $key, $parserOutput, $expire );
-
-               } else {
-                       wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" );
-               }
-       }
-
-}
diff --git a/includes/ParserOptions.php b/includes/ParserOptions.php
deleted file mode 100644 (file)
index 330ec44..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-
-/**
- * Set options of the Parser
- * @todo document
- * @ingroup Parser
- */
-class ParserOptions
-{
-       # All variables are supposed to be private in theory, although in practise this is not the case.
-       var $mUseTeX;                    # Use texvc to expand <math> tags
-       var $mUseDynamicDates;           # Use DateFormatter to format dates
-       var $mInterwikiMagic;            # Interlanguage links are removed and returned in an array
-       var $mAllowExternalImages;       # Allow external images inline
-       var $mAllowExternalImagesFrom;   # If not, any exception?
-       var $mSkin;                      # Reference to the preferred skin
-       var $mDateFormat;                # Date format index
-       var $mEditSection;               # Create "edit section" links
-       var $mNumberHeadings;            # Automatically number headings
-       var $mAllowSpecialInclusion;     # Allow inclusion of special pages
-       var $mTidy;                      # Ask for tidy cleanup
-       var $mInterfaceMessage;          # Which lang to call for PLURAL and GRAMMAR
-       var $mTargetLanguage;            # Overrides above setting with arbitrary language
-       var $mMaxIncludeSize;            # Maximum size of template expansions, in bytes
-       var $mMaxPPNodeCount;            # Maximum number of nodes touched by PPFrame::expand()
-       var $mMaxPPExpandDepth;          # Maximum recursion depth in PPFrame::expand()
-       var $mMaxTemplateDepth;          # Maximum recursion depth for templates within templates
-       var $mRemoveComments;            # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
-       var $mTemplateCallback;          # Callback for template fetching
-       var $mEnableLimitReport;         # Enable limit report in an HTML comment on output
-       var $mTimestamp;                 # Timestamp used for {{CURRENTDAY}} etc.
-
-       var $mUser;                      # Stored user object, just used to initialise the skin
-
-       function getUseTeX()                        { return $this->mUseTeX; }
-       function getUseDynamicDates()               { return $this->mUseDynamicDates; }
-       function getInterwikiMagic()                { return $this->mInterwikiMagic; }
-       function getAllowExternalImages()           { return $this->mAllowExternalImages; }
-       function getAllowExternalImagesFrom()       { return $this->mAllowExternalImagesFrom; }
-       function getEditSection()                   { return $this->mEditSection; }
-       function getNumberHeadings()                { return $this->mNumberHeadings; }
-       function getAllowSpecialInclusion()         { return $this->mAllowSpecialInclusion; }
-       function getTidy()                          { return $this->mTidy; }
-       function getInterfaceMessage()              { return $this->mInterfaceMessage; }
-       function getTargetLanguage()                { return $this->mTargetLanguage; }
-       function getMaxIncludeSize()                { return $this->mMaxIncludeSize; }
-       function getMaxPPNodeCount()                { return $this->mMaxPPNodeCount; }
-       function getMaxTemplateDepth()              { return $this->mMaxTemplateDepth; }
-       function getRemoveComments()                { return $this->mRemoveComments; }
-       function getTemplateCallback()              { return $this->mTemplateCallback; }
-       function getEnableLimitReport()             { return $this->mEnableLimitReport; }
-
-       function getSkin() {
-               if ( !isset( $this->mSkin ) ) {
-                       $this->mSkin = $this->mUser->getSkin();
-               }
-               return $this->mSkin;
-       }
-
-       function getDateFormat() {
-               if ( !isset( $this->mDateFormat ) ) {
-                       $this->mDateFormat = $this->mUser->getDatePreference();
-               }
-               return $this->mDateFormat;
-       }
-
-       function getTimestamp() {
-               if ( !isset( $this->mTimestamp ) ) {
-                       $this->mTimestamp = wfTimestampNow();
-               }
-               return $this->mTimestamp;
-       }
-
-       function setUseTeX( $x )                    { return wfSetVar( $this->mUseTeX, $x ); }
-       function setUseDynamicDates( $x )           { return wfSetVar( $this->mUseDynamicDates, $x ); }
-       function setInterwikiMagic( $x )            { return wfSetVar( $this->mInterwikiMagic, $x ); }
-       function setAllowExternalImages( $x )       { return wfSetVar( $this->mAllowExternalImages, $x ); }
-       function setAllowExternalImagesFrom( $x )   { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
-       function setDateFormat( $x )                { return wfSetVar( $this->mDateFormat, $x ); }
-       function setEditSection( $x )               { return wfSetVar( $this->mEditSection, $x ); }
-       function setNumberHeadings( $x )            { return wfSetVar( $this->mNumberHeadings, $x ); }
-       function setAllowSpecialInclusion( $x )     { return wfSetVar( $this->mAllowSpecialInclusion, $x ); }
-       function setTidy( $x )                      { return wfSetVar( $this->mTidy, $x); }
-       function setSkin( $x )                      { $this->mSkin = $x; }
-       function setInterfaceMessage( $x )          { return wfSetVar( $this->mInterfaceMessage, $x); }
-       function setTargetLanguage( $x )            { return wfSetVar( $this->mTargetLanguage, $x); }
-       function setMaxIncludeSize( $x )            { return wfSetVar( $this->mMaxIncludeSize, $x ); }
-       function setMaxPPNodeCount( $x )            { return wfSetVar( $this->mMaxPPNodeCount, $x ); }
-       function setMaxTemplateDepth( $x )          { return wfSetVar( $this->mMaxTemplateDepth, $x ); }
-       function setRemoveComments( $x )            { return wfSetVar( $this->mRemoveComments, $x ); }
-       function setTemplateCallback( $x )          { return wfSetVar( $this->mTemplateCallback, $x ); }
-       function enableLimitReport( $x = true )     { return wfSetVar( $this->mEnableLimitReport, $x ); }
-       function setTimestamp( $x )                 { return wfSetVar( $this->mTimestamp, $x ); }
-
-       function __construct( $user = null ) {
-               $this->initialiseFromUser( $user );
-       }
-
-       /**
-        * Get parser options
-        * @static
-        */
-       static function newFromUser( $user ) {
-               return new ParserOptions( $user );
-       }
-
-       /** Get user options */
-       function initialiseFromUser( $userInput ) {
-               global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
-               global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize;
-               global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth;
-               $fname = 'ParserOptions::initialiseFromUser';
-               wfProfileIn( $fname );
-               if ( !$userInput ) {
-                       global $wgUser;
-                       if ( isset( $wgUser ) ) {
-                               $user = $wgUser;
-                       } else {
-                               $user = new User;
-                       }
-               } else {
-                       $user =& $userInput;
-               }
-
-               $this->mUser = $user;
-
-               $this->mUseTeX = $wgUseTeX;
-               $this->mUseDynamicDates = $wgUseDynamicDates;
-               $this->mInterwikiMagic = $wgInterwikiMagic;
-               $this->mAllowExternalImages = $wgAllowExternalImages;
-               $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
-               $this->mSkin = null; # Deferred
-               $this->mDateFormat = null; # Deferred
-               $this->mEditSection = true;
-               $this->mNumberHeadings = $user->getOption( 'numberheadings' );
-               $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
-               $this->mTidy = false;
-               $this->mInterfaceMessage = false;
-               $this->mTargetLanguage = null; // default depends on InterfaceMessage setting
-               $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
-               $this->mMaxPPNodeCount = $wgMaxPPNodeCount;
-               $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
-               $this->mMaxTemplateDepth = $wgMaxTemplateDepth;
-               $this->mRemoveComments = true;
-               $this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' );
-               $this->mEnableLimitReport = false;
-               wfProfileOut( $fname );
-       }
-}
diff --git a/includes/ParserOutput.php b/includes/ParserOutput.php
deleted file mode 100644 (file)
index f98d564..0000000
+++ /dev/null
@@ -1,206 +0,0 @@
-<?php
-/**
- * @todo document
- * @ingroup Parser
- */
-class ParserOutput
-{
-       var $mText,             # The output text
-               $mLanguageLinks,    # List of the full text of language links, in the order they appear
-               $mCategories,       # Map of category names to sort keys
-               $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
-               $mCacheTime,        # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
-               $mVersion,          # Compatibility check
-               $mTitleText,        # title text of the chosen language variant
-               $mLinks,            # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
-               $mTemplates,        # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
-               $mTemplateIds,      # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken.
-               $mImages,           # DB keys of the images used, in the array key only
-               $mExternalLinks,    # External link URLs, in the key only
-               $mNewSection,       # Show a new section link?
-               $mNoGallery,        # No gallery on category page? (__NOGALLERY__)
-               $mHeadItems,        # Items to put in the <head> section
-               $mOutputHooks,      # Hook tags as per $wgParserOutputHooks
-               $mWarnings,         # Warning text to be returned to the user. Wikitext formatted, in the key only
-               $mSections,         # Table of contents
-               $mProperties;       # Name/value pairs to be cached in the DB
-
-       /**
-        * Overridden title for display
-        */
-       private $displayTitle = false;
-
-       function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
-               $containsOldMagic = false, $titletext = '' )
-       {
-               $this->mText = $text;
-               $this->mLanguageLinks = $languageLinks;
-               $this->mCategories = $categoryLinks;
-               $this->mContainsOldMagic = $containsOldMagic;
-               $this->mCacheTime = '';
-               $this->mVersion = Parser::VERSION;
-               $this->mTitleText = $titletext;
-               $this->mSections = array();
-               $this->mLinks = array();
-               $this->mTemplates = array();
-               $this->mImages = array();
-               $this->mExternalLinks = array();
-               $this->mNewSection = false;
-               $this->mNoGallery = false;
-               $this->mHeadItems = array();
-               $this->mTemplateIds = array();
-               $this->mOutputHooks = array();
-               $this->mWarnings = array();
-               $this->mProperties = array();
-       }
-
-       function getText()                   { return $this->mText; }
-       function &getLanguageLinks()         { return $this->mLanguageLinks; }
-       function getCategoryLinks()          { return array_keys( $this->mCategories ); }
-       function &getCategories()            { return $this->mCategories; }
-       function getCacheTime()              { return $this->mCacheTime; }
-       function getTitleText()              { return $this->mTitleText; }
-       function getSections()               { return $this->mSections; }
-       function &getLinks()                 { return $this->mLinks; }
-       function &getTemplates()             { return $this->mTemplates; }
-       function &getImages()                { return $this->mImages; }
-       function &getExternalLinks()         { return $this->mExternalLinks; }
-       function getNoGallery()              { return $this->mNoGallery; }
-       function getSubtitle()               { return $this->mSubtitle; }
-       function getOutputHooks()            { return (array)$this->mOutputHooks; }
-       function getWarnings()               { return array_keys( $this->mWarnings ); }
-
-       function containsOldMagic()          { return $this->mContainsOldMagic; }
-       function setText( $text )            { return wfSetVar( $this->mText, $text ); }
-       function setLanguageLinks( $ll )     { return wfSetVar( $this->mLanguageLinks, $ll ); }
-       function setCategoryLinks( $cl )     { return wfSetVar( $this->mCategories, $cl ); }
-       function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
-       function setCacheTime( $t )          { return wfSetVar( $this->mCacheTime, $t ); }
-       function setTitleText( $t )          { return wfSetVar( $this->mTitleText, $t ); }
-       function setSections( $toc )         { return wfSetVar( $this->mSections, $toc ); }
-
-       function addCategory( $c, $sort )    { $this->mCategories[$c] = $sort; }
-       function addLanguageLink( $t )       { $this->mLanguageLinks[] = $t; }
-       function addExternalLink( $url )     { $this->mExternalLinks[$url] = 1; }
-       function addWarning( $s )            { $this->mWarnings[$s] = 1; }
-
-       function addOutputHook( $hook, $data = false ) {
-               $this->mOutputHooks[] = array( $hook, $data );
-       }
-
-       function setNewSection( $value ) {
-               $this->mNewSection = (bool)$value;
-       }
-       function getNewSection() {
-               return (bool)$this->mNewSection;
-       }
-
-       function addLink( $title, $id = null ) {
-               $ns = $title->getNamespace();
-               $dbk = $title->getDBkey();
-               if ( !isset( $this->mLinks[$ns] ) ) {
-                       $this->mLinks[$ns] = array();
-               }
-               if ( is_null( $id ) ) {
-                       $id = $title->getArticleID();
-               }
-               $this->mLinks[$ns][$dbk] = $id;
-       }
-
-       function addImage( $name ) {
-               $this->mImages[$name] = 1;
-       }
-
-       function addTemplate( $title, $page_id, $rev_id ) {
-               $ns = $title->getNamespace();
-               $dbk = $title->getDBkey();
-               if ( !isset( $this->mTemplates[$ns] ) ) {
-                       $this->mTemplates[$ns] = array();
-               }
-               $this->mTemplates[$ns][$dbk] = $page_id;
-               if ( !isset( $this->mTemplateIds[$ns] ) ) {
-                       $this->mTemplateIds[$ns] = array();
-               }
-               $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
-       }
-
-       /**
-        * Return true if this cached output object predates the global or
-        * per-article cache invalidation timestamps, or if it comes from
-        * an incompatible older version.
-        *
-        * @param string $touched the affected article's last touched timestamp
-        * @return bool
-        * @public
-        */
-       function expired( $touched ) {
-               global $wgCacheEpoch;
-               return $this->getCacheTime() == -1 || // parser says it's uncacheable
-                      $this->getCacheTime() < $touched ||
-                      $this->getCacheTime() <= $wgCacheEpoch ||
-                      !isset( $this->mVersion ) ||
-                      version_compare( $this->mVersion, Parser::VERSION, "lt" );
-       }
-
-       /**
-        * Add some text to the <head>.
-        * If $tag is set, the section with that tag will only be included once
-        * in a given page.
-        */
-       function addHeadItem( $section, $tag = false ) {
-               if ( $tag !== false ) {
-                       $this->mHeadItems[$tag] = $section;
-               } else {
-                       $this->mHeadItems[] = $section;
-               }
-       }
-
-       /**
-        * Override the title to be used for display
-        * -- this is assumed to have been validated
-        * (check equal normalisation, etc.)
-        *
-        * @param string $text Desired title text
-        */
-       public function setDisplayTitle( $text ) {
-               $this->displayTitle = $text;
-       }
-
-       /**
-        * Get the title to be used for display
-        *
-        * @return string
-        */
-       public function getDisplayTitle() {
-               return $this->displayTitle;
-       }
-
-       /**
-        * Fairly generic flag setter thingy.
-        */
-       public function setFlag( $flag ) {
-               $this->mFlags[$flag] = true;
-       }
-
-       public function getFlag( $flag ) {
-               return isset( $this->mFlags[$flag] );
-       }
-
-       /**
-        * Set a property to be cached in the DB
-        */
-       public function setProperty( $name, $value ) {
-               $this->mProperties[$name] = $value;
-       }
-
-       public function getProperty( $name ){
-               return isset( $this->mProperties[$name] ) ? $this->mProperties[$name] : false;
-       }
-
-       public function getProperties() {
-               if ( !isset( $this->mProperties ) ) {
-                       $this->mProperties = array();
-               }
-               return $this->mProperties;
-       }
-}
diff --git a/includes/Parser_DiffTest.php b/includes/Parser_DiffTest.php
deleted file mode 100644 (file)
index be3702c..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-/**
- * @ingroup Parser
- */
-class Parser_DiffTest
-{
-       var $parsers, $conf;
-
-       var $dfUniqPrefix;
-
-       function __construct( $conf ) {
-               if ( !isset( $conf['parsers'] ) ) {
-                       throw new MWException( __METHOD__ . ': no parsers specified' );
-               }
-               $this->conf = $conf;
-               $this->dtUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
-       }
-
-       function init() {
-               if ( !is_null( $this->parsers ) ) {
-                       return;
-               }
-
-               global $wgHooks;
-               static $doneHook = false;
-               if ( !$doneHook ) {
-                       $doneHook = true;
-                       $wgHooks['ParserClearState'][] = array( $this, 'onClearState' );
-               }
-
-               foreach ( $this->conf['parsers'] as $i => $parserConf ) {
-                       if ( !is_array( $parserConf ) ) {
-                               $class = $parserConf;
-                               $parserConf = array( 'class' => $parserConf );
-                       } else {
-                               $class = $parserConf['class'];
-                       }
-                       $this->parsers[$i] = new $class( $parserConf );
-               }
-       }
-
-       function __call( $name, $args ) {
-               $this->init();
-               $results = array();
-               $mismatch = false;
-               $lastResult = null;
-               $first = true;
-               foreach ( $this->parsers as $i => $parser ) {
-                       $currentResult = call_user_func_array( array( &$this->parsers[$i], $name ), $args );
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               if ( is_object( $lastResult ) ) {
-                                       if ( $lastResult != $currentResult ) {
-                                               $mismatch = true;
-                                       }
-                               } else {
-                                       if ( $lastResult !== $currentResult ) {
-                                               $mismatch = true;
-                                       }
-                               }
-                       }
-                       $results[$i] = $currentResult;
-                       $lastResult = $currentResult;
-               }
-               if ( $mismatch ) {
-                       throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" .
-                               'Arguments: ' . var_export( $args, true ) . "\n" .
-                               'Results: ' . var_export( $results, true ) . "\n" );
-               }
-               return $lastResult;
-       }
-
-       function setFunctionHook( $id, $callback, $flags = 0 ) {
-               $this->init();
-               foreach  ( $this->parsers as $i => $parser ) {
-                       $parser->setFunctionHook( $id, $callback, $flags );
-               }
-       }
-
-       function onClearState( &$parser ) {
-               // hack marker prefixes to get identical output
-               $parser->mUniqPrefix = $this->dtUniqPrefix;
-               return true;
-       }
-}
diff --git a/includes/Parser_OldPP.php b/includes/Parser_OldPP.php
deleted file mode 100644 (file)
index 0d0394a..0000000
+++ /dev/null
@@ -1,4944 +0,0 @@
-<?php
-/**
- * Parser with old preprocessor
- * @ingroup Parser
- */
-class Parser_OldPP
-{
-       /**
-        * Update this version number when the ParserOutput format
-        * changes in an incompatible way, so the parser cache
-        * can automatically discard old data.
-        */
-       const VERSION = '1.6.4';
-
-       # Flags for Parser::setFunctionHook
-       # Also available as global constants from Defines.php
-       const SFH_NO_HASH = 1;
-
-       # Constants needed for external link processing
-       # Everything except bracket, space, or control characters
-       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
-       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/S';
-
-       // State constants for the definition list colon extraction
-       const COLON_STATE_TEXT = 0;
-       const COLON_STATE_TAG = 1;
-       const COLON_STATE_TAGSTART = 2;
-       const COLON_STATE_CLOSETAG = 3;
-       const COLON_STATE_TAGSLASH = 4;
-       const COLON_STATE_COMMENT = 5;
-       const COLON_STATE_COMMENTDASH = 6;
-       const COLON_STATE_COMMENTDASHDASH = 7;
-
-       // Allowed values for $this->mOutputType
-       // Parameter to startExternalParse().
-       const OT_HTML = 1;
-       const OT_WIKI = 2;
-       const OT_PREPROCESS = 3;
-       const OT_MSG = 4;
-
-       /**#@+
-        * @private
-        */
-       # Persistent:
-       var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
-               $mImageParams, $mImageParamsMagicArray, $mExtLinkBracketedRegex;
-
-       # Cleared with clearState():
-       var $mOutput, $mAutonumber, $mDTopen, $mStripState;
-       var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
-       var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix;
-       var $mIncludeSizes, $mDefaultSort;
-       var $mTemplates,        // cache of already loaded templates, avoids
-                               // multiple SQL queries for the same string
-           $mTemplatePath;     // stores an unsorted hash of all the templates already loaded
-                               // in this path. Used for loop detection.
-
-       # Temporary
-       # These are variables reset at least once per parse regardless of $clearState
-       var $mOptions,      // ParserOptions object
-               $mTitle,        // Title context, used for self-link rendering and similar things
-               $mOutputType,   // Output type, one of the OT_xxx constants
-               $ot,            // Shortcut alias, see setOutputType()
-               $mRevisionId,   // ID to display in {{REVISIONID}} tags
-               $mRevisionTimestamp, // The timestamp of the specified revision ID
-               $mRevIdForTs;   // The revision ID which was used to fetch the timestamp
-
-       /**#@-*/
-
-       /**
-        * Constructor
-        *
-        * @public
-        */
-       function __construct( $conf = array() ) {
-               $this->mTagHooks = array();
-               $this->mTransparentTagHooks = array();
-               $this->mFunctionHooks = array();
-               $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
-               $this->mFirstCall = true;
-               $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
-                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
-       }
-
-       /**
-        * Do various kinds of initialisation on the first call of the parser
-        */
-       function firstCallInit() {
-               if ( !$this->mFirstCall ) {
-                       return;
-               }
-               $this->mFirstCall = false;
-
-               wfProfileIn( __METHOD__ );
-               global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
-
-               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
-
-               # Syntax for arguments (see self::setFunctionHook):
-               #  "name for lookup in localized magic words array",
-               #  function callback,
-               #  optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}
-               #    instead of {{#int:...}})
-               $this->setFunctionHook( 'int',              array( 'CoreParserFunctions', 'intFunction'      ), SFH_NO_HASH );
-               $this->setFunctionHook( 'ns',               array( 'CoreParserFunctions', 'ns'               ), SFH_NO_HASH );
-               $this->setFunctionHook( 'urlencode',        array( 'CoreParserFunctions', 'urlencode'        ), SFH_NO_HASH );
-               $this->setFunctionHook( 'lcfirst',          array( 'CoreParserFunctions', 'lcfirst'          ), SFH_NO_HASH );
-               $this->setFunctionHook( 'ucfirst',          array( 'CoreParserFunctions', 'ucfirst'          ), SFH_NO_HASH );
-               $this->setFunctionHook( 'lc',               array( 'CoreParserFunctions', 'lc'               ), SFH_NO_HASH );
-               $this->setFunctionHook( 'uc',               array( 'CoreParserFunctions', 'uc'               ), SFH_NO_HASH );
-               $this->setFunctionHook( 'localurl',         array( 'CoreParserFunctions', 'localurl'         ), SFH_NO_HASH );
-               $this->setFunctionHook( 'localurle',        array( 'CoreParserFunctions', 'localurle'        ), SFH_NO_HASH );
-               $this->setFunctionHook( 'fullurl',          array( 'CoreParserFunctions', 'fullurl'          ), SFH_NO_HASH );
-               $this->setFunctionHook( 'fullurle',         array( 'CoreParserFunctions', 'fullurle'         ), SFH_NO_HASH );
-               $this->setFunctionHook( 'formatnum',        array( 'CoreParserFunctions', 'formatnum'        ), SFH_NO_HASH );
-               $this->setFunctionHook( 'grammar',          array( 'CoreParserFunctions', 'grammar'          ), SFH_NO_HASH );
-               $this->setFunctionHook( 'plural',           array( 'CoreParserFunctions', 'plural'           ), SFH_NO_HASH );
-               $this->setFunctionHook( 'numberofpages',    array( 'CoreParserFunctions', 'numberofpages'    ), SFH_NO_HASH );
-               $this->setFunctionHook( 'numberofusers',    array( 'CoreParserFunctions', 'numberofusers'    ), SFH_NO_HASH );
-               $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH );
-               $this->setFunctionHook( 'numberoffiles',    array( 'CoreParserFunctions', 'numberoffiles'    ), SFH_NO_HASH );
-               $this->setFunctionHook( 'numberofadmins',   array( 'CoreParserFunctions', 'numberofadmins'   ), SFH_NO_HASH );
-               $this->setFunctionHook( 'numberofedits',    array( 'CoreParserFunctions', 'numberofedits'    ), SFH_NO_HASH );
-               $this->setFunctionHook( 'language',         array( 'CoreParserFunctions', 'language'         ), SFH_NO_HASH );
-               $this->setFunctionHook( 'padleft',          array( 'CoreParserFunctions', 'padleft'          ), SFH_NO_HASH );
-               $this->setFunctionHook( 'padright',         array( 'CoreParserFunctions', 'padright'         ), SFH_NO_HASH );
-               $this->setFunctionHook( 'anchorencode',     array( 'CoreParserFunctions', 'anchorencode'     ), SFH_NO_HASH );
-               $this->setFunctionHook( 'special',          array( 'CoreParserFunctions', 'special'          ) );
-               $this->setFunctionHook( 'defaultsort',      array( 'CoreParserFunctions', 'defaultsort'      ), SFH_NO_HASH );
-               $this->setFunctionHook( 'filepath',         array( 'CoreParserFunctions', 'filepath'         ), SFH_NO_HASH );
-
-               if ( $wgAllowDisplayTitle ) {
-                       $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
-               }
-               if ( $wgAllowSlowParserFunctions ) {
-                       $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH );
-               }
-
-               $this->initialiseVariables();
-
-               wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Clear Parser state
-        *
-        * @private
-        */
-       function clearState() {
-               wfProfileIn( __METHOD__ );
-               if ( $this->mFirstCall ) {
-                       $this->firstCallInit();
-               }
-               $this->mOutput = new ParserOutput;
-               $this->mAutonumber = 0;
-               $this->mLastSection = '';
-               $this->mDTopen = false;
-               $this->mIncludeCount = array();
-               $this->mStripState = new StripState;
-               $this->mArgStack = array();
-               $this->mInPre = false;
-               $this->mInterwikiLinkHolders = array(
-                       'texts' => array(),
-                       'titles' => array()
-               );
-               $this->mLinkHolders = array(
-                       'namespaces' => array(),
-                       'dbkeys' => array(),
-                       'queries' => array(),
-                       'texts' => array(),
-                       'titles' => array()
-               );
-               $this->mRevisionTimestamp = $this->mRevisionId = null;
-
-               /**
-                * Prefix for temporary replacement strings for the multipass parser.
-                * \x07 should never appear in input as it's disallowed in XML.
-                * Using it at the front also gives us a little extra robustness
-                * since it shouldn't match when butted up against identifier-like
-                * string constructs.
-                */
-               $this->mUniqPrefix = "\x07UNIQ" . self::getRandomString();
-
-               # Clear these on every parse, bug 4549
-               $this->mTemplates = array();
-               $this->mTemplatePath = array();
-
-               $this->mShowToc = true;
-               $this->mForceTocPosition = false;
-               $this->mIncludeSizes = array(
-                       'pre-expand' => 0,
-                       'post-expand' => 0,
-                       'arg' => 0
-               );
-               $this->mDefaultSort = false;
-
-               wfRunHooks( 'ParserClearState', array( &$this ) );
-               wfProfileOut( __METHOD__ );
-       }
-
-       function setOutputType( $ot ) {
-               $this->mOutputType = $ot;
-               // Shortcut alias
-               $this->ot = array(
-                       'html' => $ot == self::OT_HTML,
-                       'wiki' => $ot == self::OT_WIKI,
-                       'msg' => $ot == self::OT_MSG,
-                       'pre' => $ot == self::OT_PREPROCESS,
-               );
-       }
-
-       /**
-        * Accessor for mUniqPrefix.
-        *
-        * @public
-        */
-       function uniqPrefix() {
-               return $this->mUniqPrefix;
-       }
-
-       /**
-        * Convert wikitext to HTML
-        * Do not call this function recursively.
-        *
-        * @param string $text Text we want to parse
-        * @param Title &$title A title object
-        * @param array $options
-        * @param boolean $linestart
-        * @param boolean $clearState
-        * @param int $revid number to pass in {{REVISIONID}}
-        * @return ParserOutput a ParserOutput
-        */
-       public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
-               /**
-                * First pass--just handle <nowiki> sections, pass the rest off
-                * to internalParse() which does all the real work.
-                */
-
-               global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
-               $fname = 'Parser::parse-' . wfGetCaller();
-               wfProfileIn( __METHOD__ );
-               wfProfileIn( $fname );
-
-               if ( $clearState ) {
-                       $this->clearState();
-               }
-
-               $this->mOptions = $options;
-               $this->mTitle =& $title;
-               $oldRevisionId = $this->mRevisionId;
-               $oldRevisionTimestamp = $this->mRevisionTimestamp;
-               if( $revid !== null ) {
-                       $this->mRevisionId = $revid;
-                       $this->mRevisionTimestamp = null;
-               }
-               $this->setOutputType( self::OT_HTML );
-               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->strip( $text, $this->mStripState );
-               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->internalParse( $text );
-               $text = $this->mStripState->unstripGeneral( $text );
-
-               # Clean up special characters, only run once, next-to-last before doBlockLevels
-               $fixtags = array(
-                       # french spaces, last one Guillemet-left
-                       # only if there is something before the space
-                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
-                       # french spaces, Guillemet-right
-                       '/(\\302\\253) /' => '\\1&nbsp;',
-               );
-               $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
-
-               # only once and last
-               $text = $this->doBlockLevels( $text, $linestart );
-
-               $this->replaceLinkHolders( $text );
-
-               # the position of the parserConvert() call should not be changed. it
-               # assumes that the links are all replaced and the only thing left
-               # is the <nowiki> mark.
-               # Side-effects: this calls $this->mOutput->setTitleText()
-               $text = $wgContLang->parserConvert( $text, $this );
-
-               $text = $this->mStripState->unstripNoWiki( $text );
-
-               wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
-
-//!JF Move to its own function
-
-               $uniq_prefix = $this->mUniqPrefix;
-                $matches = array();
-               $elements = array_keys( $this->mTransparentTagHooks );
-                $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-
-                foreach( $matches as $marker => $data ) {
-                        list( $element, $content, $params, $tag ) = $data;
-                        $tagName = strtolower( $element );
-                        if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
-                                $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
-                                        array( $content, $params, $this ) );
-                        } else {
-                               $output = $tag;
-                       }
-                       $this->mStripState->general->setPair( $marker, $output );
-               }
-               $text = $this->mStripState->unstripGeneral( $text );
-
-               $text = Sanitizer::normalizeCharReferences( $text );
-
-               if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
-                       $text = self::tidy($text);
-               } else {
-                       # attempt to sanitize at least some nesting problems
-                       # (bug #2702 and quite a few others)
-                       $tidyregs = array(
-                               # ''Something [http://www.cool.com cool''] -->
-                               # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
-                               '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
-                               '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
-                               # fix up an anchor inside another anchor, only
-                               # at least for a single single nested link (bug 3695)
-                               '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
-                               '\\1\\2</a>\\3</a>\\1\\4</a>',
-                               # fix div inside inline elements- doBlockLevels won't wrap a line which
-                               # contains a div, so fix it up here; replace
-                               # div with escaped text
-                               '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
-                               '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
-                               # remove empty italic or bold tag pairs, some
-                               # introduced by rules above
-                               '/<([bi])><\/\\1>/' => '',
-                       );
-
-                       $text = preg_replace(
-                               array_keys( $tidyregs ),
-                               array_values( $tidyregs ),
-                               $text );
-               }
-
-               wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
-
-               # Information on include size limits, for the benefit of users who try to skirt them
-               if ( $this->mOptions->getEnableLimitReport() ) {
-                       $max = $this->mOptions->getMaxIncludeSize();
-                       $limitReport =
-                               "Pre-expand include size: {$this->mIncludeSizes['pre-expand']}/$max bytes\n" .
-                               "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
-                               "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n";
-                       wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
-                       $text .= "<!-- \n$limitReport-->\n";
-               }
-               $this->mOutput->setText( $text );
-               $this->mRevisionId = $oldRevisionId;
-               $this->mRevisionTimestamp = $oldRevisionTimestamp;
-               wfProfileOut( $fname );
-               wfProfileOut( __METHOD__ );
-
-               return $this->mOutput;
-       }
-
-       /**
-        * Recursive parser entry point that can be called from an extension tag
-        * hook.
-        */
-       function recursiveTagParse( $text ) {
-               wfProfileIn( __METHOD__ );
-               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->strip( $text, $this->mStripState );
-               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->internalParse( $text );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       /**
-        * Expand templates and variables in the text, producing valid, static wikitext.
-        * Also removes comments.
-        */
-       function preprocess( $text, $title, $options, $revid = null ) {
-               wfProfileIn( __METHOD__ );
-               $this->clearState();
-               $this->setOutputType( self::OT_PREPROCESS );
-               $this->mOptions = $options;
-               $this->mTitle = $title;
-               if( $revid !== null ) {
-                       $this->mRevisionId = $revid;
-               }
-               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->strip( $text, $this->mStripState );
-               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               if ( $this->mOptions->getRemoveComments() ) {
-                       $text = Sanitizer::removeHTMLcomments( $text );
-               }
-               $text = $this->replaceVariables( $text );
-               $text = $this->mStripState->unstripBoth( $text );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       /**
-        * Get a random string
-        *
-        * @private
-        * @static
-        */
-       function getRandomString() {
-               return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
-       }
-
-       function &getTitle() { return $this->mTitle; }
-       function getOptions() { return $this->mOptions; }
-       function getRevisionId() { return $this->mRevisionId; }
-
-       function getFunctionLang() {
-               global $wgLang, $wgContLang;
-               return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
-       }
-
-       /**
-        * Replaces all occurrences of HTML-style comments and the given tags
-        * in the text with a random marker and returns teh next text. The output
-        * parameter $matches will be an associative array filled with data in
-        * the form:
-        *   'UNIQ-xxxxx' => array(
-        *     'element',
-        *     'tag content',
-        *     array( 'param' => 'x' ),
-        *     '<element param="x">tag content</element>' ) )
-        *
-        * @param $elements list of element names. Comments are always extracted.
-        * @param $text Source text string.
-        * @param $uniq_prefix
-        *
-        * @public
-        * @static
-        */
-       function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
-               static $n = 1;
-               $stripped = '';
-               $matches = array();
-
-               $taglist = implode( '|', $elements );
-               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
-
-               while ( '' != $text ) {
-                       $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
-                       $stripped .= $p[0];
-                       if( count( $p ) < 5 ) {
-                               break;
-                       }
-                       if( count( $p ) > 5 ) {
-                               // comment
-                               $element    = $p[4];
-                               $attributes = '';
-                               $close      = '';
-                               $inside     = $p[5];
-                       } else {
-                               // tag
-                               $element    = $p[1];
-                               $attributes = $p[2];
-                               $close      = $p[3];
-                               $inside     = $p[4];
-                       }
-
-                       $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07";
-                       $stripped .= $marker;
-
-                       if ( $close === '/>' ) {
-                               // Empty element tag, <tag />
-                               $content = null;
-                               $text = $inside;
-                               $tail = null;
-                       } else {
-                               if( $element == '!--' ) {
-                                       $end = '/(-->)/';
-                               } else {
-                                       $end = "/(<\\/$element\\s*>)/i";
-                               }
-                               $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
-                               $content = $q[0];
-                               if( count( $q ) < 3 ) {
-                                       # No end tag -- let it run out to the end of the text.
-                                       $tail = '';
-                                       $text = '';
-                               } else {
-                                       $tail = $q[1];
-                                       $text = $q[2];
-                               }
-                       }
-
-                       $matches[$marker] = array( $element,
-                               $content,
-                               Sanitizer::decodeTagAttributes( $attributes ),
-                               "<$element$attributes$close$content$tail" );
-               }
-               return $stripped;
-       }
-
-       /**
-        * Strips and renders nowiki, pre, math, hiero
-        * If $render is set, performs necessary rendering operations on plugins
-        * Returns the text, and fills an array with data needed in unstrip()
-        *
-        * @param StripState $state
-        *
-        * @param bool $stripcomments when set, HTML comments <!-- like this -->
-        *  will be stripped in addition to other tags. This is important
-        *  for section editing, where these comments cause confusion when
-        *  counting the sections in the wikisource
-        *
-        * @param array dontstrip contains tags which should not be stripped;
-        *  used to prevent stipping of <gallery> when saving (fixes bug 2700)
-        *
-        * @private
-        */
-       function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
-               global $wgContLang;
-               wfProfileIn( __METHOD__ );
-               $render = ($this->mOutputType == self::OT_HTML);
-
-               $uniq_prefix = $this->mUniqPrefix;
-               $commentState = new ReplacementArray;
-               $nowikiItems = array();
-               $generalItems = array();
-
-               $elements = array_merge(
-                       array( 'nowiki', 'gallery' ),
-                       array_keys( $this->mTagHooks ) );
-               global $wgRawHtml;
-               if( $wgRawHtml ) {
-                       $elements[] = 'html';
-               }
-               if( $this->mOptions->getUseTeX() ) {
-                       $elements[] = 'math';
-               }
-
-               # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
-               foreach ( $elements AS $k => $v ) {
-                       if ( !in_array ( $v , $dontstrip ) ) continue;
-                       unset ( $elements[$k] );
-               }
-
-               $matches = array();
-               $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
-
-               foreach( $matches as $marker => $data ) {
-                       list( $element, $content, $params, $tag ) = $data;
-                       if( $render ) {
-                               $tagName = strtolower( $element );
-                               wfProfileIn( __METHOD__."-render-$tagName" );
-                               switch( $tagName ) {
-                               case '!--':
-                                       // Comment
-                                       if( substr( $tag, -3 ) == '-->' ) {
-                                               $output = $tag;
-                                       } else {
-                                               // Unclosed comment in input.
-                                               // Close it so later stripping can remove it
-                                               $output = "$tag-->";
-                                       }
-                                       break;
-                               case 'html':
-                                       if( $wgRawHtml ) {
-                                               $output = $content;
-                                               break;
-                                       }
-                                       // Shouldn't happen otherwise. :)
-                               case 'nowiki':
-                                       $output = Xml::escapeTagsOnly( $content );
-                                       break;
-                               case 'math':
-                                       $output = $wgContLang->armourMath(
-                                               MathRenderer::renderMath( $content, $params ) );
-                                       break;
-                               case 'gallery':
-                                       $output = $this->renderImageGallery( $content, $params );
-                                       break;
-                               default:
-                                       if( isset( $this->mTagHooks[$tagName] ) ) {
-                                               $output = call_user_func_array( $this->mTagHooks[$tagName],
-                                                       array( $content, $params, $this ) );
-                                       } else {
-                                               throw new MWException( "Invalid call hook $element" );
-                                       }
-                               }
-                               wfProfileOut( __METHOD__."-render-$tagName" );
-                       } else {
-                               // Just stripping tags; keep the source
-                               $output = $tag;
-                       }
-
-                       // Unstrip the output, to support recursive strip() calls
-                       $output = $state->unstripBoth( $output );
-
-                       if( !$stripcomments && $element == '!--' ) {
-                               $commentState->setPair( $marker, $output );
-                       } elseif ( $element == 'html' || $element == 'nowiki' ) {
-                               $nowikiItems[$marker] = $output;
-                       } else {
-                               $generalItems[$marker] = $output;
-                       }
-               }
-               # Add the new items to the state
-               # We do this after the loop instead of during it to avoid slowing
-               # down the recursive unstrip
-               $state->nowiki->mergeArray( $nowikiItems );
-               $state->general->mergeArray( $generalItems );
-
-               # Unstrip comments unless explicitly told otherwise.
-               # (The comments are always stripped prior to this point, so as to
-               # not invoke any extension tags / parser hooks contained within
-               # a comment.)
-               if ( !$stripcomments ) {
-                       // Put them all back and forget them
-                       $text = $commentState->replace( $text );
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       /**
-        * Restores pre, math, and other extensions removed by strip()
-        *
-        * always call unstripNoWiki() after this one
-        * @private
-        * @deprecated use $this->mStripState->unstrip()
-        */
-       function unstrip( $text, $state ) {
-               return $state->unstripGeneral( $text );
-       }
-
-       /**
-        * Always call this after unstrip() to preserve the order
-        *
-        * @private
-        * @deprecated use $this->mStripState->unstrip()
-        */
-       function unstripNoWiki( $text, $state ) {
-               return $state->unstripNoWiki( $text );
-       }
-
-       /**
-        * @deprecated use $this->mStripState->unstripBoth()
-        */
-       function unstripForHTML( $text ) {
-               return $this->mStripState->unstripBoth( $text );
-       }
-
-       /**
-        * Add an item to the strip state
-        * Returns the unique tag which must be inserted into the stripped text
-        * The tag will be replaced with the original text in unstrip()
-        *
-        * @private
-        */
-       function insertStripItem( $text, &$state ) {
-               $rnd = $this->mUniqPrefix . '-item' . self::getRandomString();
-               $state->general->setPair( $rnd, $text );
-               return $rnd;
-       }
-
-       /**
-        * Interface with html tidy, used if $wgUseTidy = true.
-        * If tidy isn't able to correct the markup, the original will be
-        * returned in all its glory with a warning comment appended.
-        *
-        * Either the external tidy program or the in-process tidy extension
-        * will be used depending on availability. Override the default
-        * $wgTidyInternal setting to disable the internal if it's not working.
-        *
-        * @param string $text Hideous HTML input
-        * @return string Corrected HTML output
-        * @public
-        * @static
-        */
-       function tidy( $text ) {
-               global $wgTidyInternal;
-               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
-' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
-'<head><title>test</title></head><body>'.$text.'</body></html>';
-               if( $wgTidyInternal ) {
-                       $correctedtext = self::internalTidy( $wrappedtext );
-               } else {
-                       $correctedtext = self::externalTidy( $wrappedtext );
-               }
-               if( is_null( $correctedtext ) ) {
-                       wfDebug( "Tidy error detected!\n" );
-                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
-               }
-               return $correctedtext;
-       }
-
-       /**
-        * Spawn an external HTML tidy process and get corrected markup back from it.
-        *
-        * @private
-        * @static
-        */
-       function externalTidy( $text ) {
-               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
-               $fname = 'Parser::externalTidy';
-               wfProfileIn( $fname );
-
-               $cleansource = '';
-               $opts = ' -utf8';
-
-               $descriptorspec = array(
-                       0 => array('pipe', 'r'),
-                       1 => array('pipe', 'w'),
-                       2 => array('file', wfGetNull(), 'a')
-               );
-               $pipes = array();
-               $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
-               if (is_resource($process)) {
-                       // Theoretically, this style of communication could cause a deadlock
-                       // here. If the stdout buffer fills up, then writes to stdin could
-                       // block. This doesn't appear to happen with tidy, because tidy only
-                       // writes to stdout after it's finished reading from stdin. Search
-                       // for tidyParseStdin and tidySaveStdout in console/tidy.c
-                       fwrite($pipes[0], $text);
-                       fclose($pipes[0]);
-                       while (!feof($pipes[1])) {
-                               $cleansource .= fgets($pipes[1], 1024);
-                       }
-                       fclose($pipes[1]);
-                       proc_close($process);
-               }
-
-               wfProfileOut( $fname );
-
-               if( $cleansource == '' && $text != '') {
-                       // Some kind of error happened, so we couldn't get the corrected text.
-                       // Just give up; we'll use the source text and append a warning.
-                       return null;
-               } else {
-                       return $cleansource;
-               }
-       }
-
-       /**
-        * Use the HTML tidy PECL extension to use the tidy library in-process,
-        * saving the overhead of spawning a new process.
-        *
-        * 'pear install tidy' should be able to compile the extension module.
-        *
-        * @private
-        * @static
-        */
-       function internalTidy( $text ) {
-               global $wgTidyConf, $IP;
-               $fname = 'Parser::internalTidy';
-               wfProfileIn( $fname );
-
-               $tidy = new tidy;
-               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
-               $tidy->cleanRepair();
-               if( $tidy->getStatus() == 2 ) {
-                       // 2 is magic number for fatal error
-                       // http://www.php.net/manual/en/function.tidy-get-status.php
-                       $cleansource = null;
-               } else {
-                       $cleansource = tidy_get_output( $tidy );
-               }
-               wfProfileOut( $fname );
-               return $cleansource;
-       }
-
-       /**
-        * parse the wiki syntax used to render tables
-        *
-        * @private
-        */
-       function doTableStuff ( $text ) {
-               $fname = 'Parser::doTableStuff';
-               wfProfileIn( $fname );
-
-               $lines = explode ( "\n" , $text );
-               $td_history = array (); // Is currently a td tag open?
-               $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
-               $tr_history = array (); // Is currently a tr tag open?
-               $tr_attributes = array (); // history of tr attributes
-               $has_opened_tr = array(); // Did this table open a <tr> element?
-               $indent_level = 0; // indent level of the table
-               foreach ( $lines as $key => $line )
-               {
-                       $line = trim ( $line );
-
-                       if( $line == '' ) { // empty line, go to next line
-                               continue;
-                       }
-                       $first_character = $line{0};
-                       $matches = array();
-
-                       if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
-                               // First check if we are starting a new table
-                               $indent_level = strlen( $matches[1] );
-
-                               $attributes = $this->mStripState->unstripBoth( $matches[2] );
-                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
-
-                               $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
-                               array_push ( $td_history , false );
-                               array_push ( $last_tag_history , '' );
-                               array_push ( $tr_history , false );
-                               array_push ( $tr_attributes , '' );
-                               array_push ( $has_opened_tr , false );
-                       } else if ( count ( $td_history ) == 0 ) {
-                               // Don't do any of the following
-                               continue;
-                       } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
-                               // We are ending a table
-                               $line = '</table>' . substr ( $line , 2 );
-                               $last_tag = array_pop ( $last_tag_history );
-
-                               if ( !array_pop ( $has_opened_tr ) ) {
-                                       $line = "<tr><td></td></tr>{$line}";
-                               }
-
-                               if ( array_pop ( $tr_history ) ) {
-                                       $line = "</tr>{$line}";
-                               }
-
-                               if ( array_pop ( $td_history ) ) {
-                                       $line = "</{$last_tag}>{$line}";
-                               }
-                               array_pop ( $tr_attributes );
-                               $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
-                       } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
-                               // Now we have a table row
-                               $line = preg_replace( '#^\|-+#', '', $line );
-
-                               // Whats after the tag is now only attributes
-                               $attributes = $this->mStripState->unstripBoth( $line );
-                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
-                               array_pop ( $tr_attributes );
-                               array_push ( $tr_attributes , $attributes );
-
-                               $line = '';
-                               $last_tag = array_pop ( $last_tag_history );
-                               array_pop ( $has_opened_tr );
-                               array_push ( $has_opened_tr , true );
-
-                               if ( array_pop ( $tr_history ) ) {
-                                       $line = '</tr>';
-                               }
-
-                               if ( array_pop ( $td_history ) ) {
-                                       $line = "</{$last_tag}>{$line}";
-                               }
-
-                               $lines[$key] = $line;
-                               array_push ( $tr_history , false );
-                               array_push ( $td_history , false );
-                               array_push ( $last_tag_history , '' );
-                       }
-                       else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 )  == '|+' ) {
-                               // This might be cell elements, td, th or captions
-                               if ( substr ( $line , 0 , 2 ) == '|+' ) {
-                                       $first_character = '+';
-                                       $line = substr ( $line , 1 );
-                               }
-
-                               $line = substr ( $line , 1 );
-
-                               if ( $first_character == '!' ) {
-                                       $line = str_replace ( '!!' , '||' , $line );
-                               }
-
-                               // Split up multiple cells on the same line.
-                               // FIXME : This can result in improper nesting of tags processed
-                               // by earlier parser steps, but should avoid splitting up eg
-                               // attribute values containing literal "||".
-                               $cells = StringUtils::explodeMarkup( '||' , $line );
-
-                               $lines[$key] = '';
-
-                               // Loop through each table cell
-                               foreach ( $cells as $cell )
-                               {
-                                       $previous = '';
-                                       if ( $first_character != '+' )
-                                       {
-                                               $tr_after = array_pop ( $tr_attributes );
-                                               if ( !array_pop ( $tr_history ) ) {
-                                                       $previous = "<tr{$tr_after}>\n";
-                                               }
-                                               array_push ( $tr_history , true );
-                                               array_push ( $tr_attributes , '' );
-                                               array_pop ( $has_opened_tr );
-                                               array_push ( $has_opened_tr , true );
-                                       }
-
-                                       $last_tag = array_pop ( $last_tag_history );
-
-                                       if ( array_pop ( $td_history ) ) {
-                                               $previous = "</{$last_tag}>{$previous}";
-                                       }
-
-                                       if ( $first_character == '|' ) {
-                                               $last_tag = 'td';
-                                       } else if ( $first_character == '!' ) {
-                                               $last_tag = 'th';
-                                       } else if ( $first_character == '+' ) {
-                                               $last_tag = 'caption';
-                                       } else {
-                                               $last_tag = '';
-                                       }
-
-                                       array_push ( $last_tag_history , $last_tag );
-
-                                       // A cell could contain both parameters and data
-                                       $cell_data = explode ( '|' , $cell , 2 );
-
-                                       // Bug 553: Note that a '|' inside an invalid link should not
-                                       // be mistaken as delimiting cell parameters
-                                       if ( strpos( $cell_data[0], '[[' ) !== false ) {
-                                               $cell = "{$previous}<{$last_tag}>{$cell}";
-                                       } else if ( count ( $cell_data ) == 1 )
-                                               $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
-                                       else {
-                                               $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
-                                               $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
-                                               $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
-                                       }
-
-                                       $lines[$key] .= $cell;
-                                       array_push ( $td_history , true );
-                               }
-                       }
-               }
-
-               // Closing open td, tr && table
-               while ( count ( $td_history ) > 0 )
-               {
-                       if ( array_pop ( $td_history ) ) {
-                               $lines[] = '</td>' ;
-                       }
-                       if ( array_pop ( $tr_history ) ) {
-                               $lines[] = '</tr>' ;
-                       }
-                       if ( !array_pop ( $has_opened_tr ) ) {
-                               $lines[] = "<tr><td></td></tr>" ;
-                       }
-
-                       $lines[] = '</table>' ;
-               }
-
-               $output = implode ( "\n" , $lines ) ;
-
-               // special case: don't return empty table
-               if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
-                       $output = '';
-               }
-
-               wfProfileOut( $fname );
-
-               return $output;
-       }
-
-       /**
-        * Helper function for parse() that transforms wiki markup into
-        * HTML. Only called for $mOutputType == OT_HTML.
-        *
-        * @private
-        */
-       function internalParse( $text ) {
-               $args = array();
-               $isMain = true;
-               $fname = 'Parser::internalParse';
-               wfProfileIn( $fname );
-
-               # Hook to suspend the parser in this state
-               if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
-                       wfProfileOut( $fname );
-                       return $text ;
-               }
-
-               # Remove <noinclude> tags and <includeonly> sections
-               $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) );
-               $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') );
-               $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text );
-
-               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) );
-
-               $text = $this->replaceVariables( $text, $args );
-               wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
-
-               // Tables need to come after variable replacement for things to work
-               // properly; putting them before other transformations should keep
-               // exciting things like link expansions from showing up in surprising
-               // places.
-               $text = $this->doTableStuff( $text );
-
-               $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
-
-               $text = $this->stripToc( $text );
-               $this->stripNoGallery( $text );
-               $text = $this->doHeadings( $text );
-               if($this->mOptions->getUseDynamicDates()) {
-                       $df =& DateFormatter::getInstance();
-                       $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
-               }
-               $text = $this->doAllQuotes( $text );
-               $text = $this->replaceInternalLinks( $text );
-               $text = $this->replaceExternalLinks( $text );
-
-               # replaceInternalLinks may sometimes leave behind
-               # absolute URLs, which have to be masked to hide them from replaceExternalLinks
-               $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
-
-               $text = $this->doMagicLinks( $text );
-               $text = $this->formatHeadings( $text, $isMain );
-
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * Replace special strings like "ISBN xxx" and "RFC xxx" with
-        * magic external links.
-        *
-        * @private
-        */
-       function &doMagicLinks( &$text ) {
-               wfProfileIn( __METHOD__ );
-               $text = preg_replace_callback(
-                       '!(?:                           # Start cases
-                           <a.*?</a> |                 # Skip link text
-                           <.*?> |                     # Skip stuff inside HTML elements
-                           (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
-                           ISBN\s+(\b                  # ISBN, capture number as m[2]
-                                     (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
-                                     (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
-                                     [0-9Xx]                 # check digit
-                                   \b)
-                       )!x', array( &$this, 'magicLinkCallback' ), $text );
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       function magicLinkCallback( $m ) {
-               if ( substr( $m[0], 0, 1 ) == '<' ) {
-                       # Skip HTML element
-                       return $m[0];
-               } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
-                       $isbn = $m[2];
-                       $num = strtr( $isbn, array(
-                               '-' => '',
-                               ' ' => '',
-                               'x' => 'X',
-                       ));
-                       $titleObj = SpecialPage::getTitleFor( 'Booksources' );
-                       $text = '<a href="' .
-                               $titleObj->escapeLocalUrl( "isbn=$num" ) .
-                               "\" class=\"internal\">ISBN $isbn</a>";
-               } else {
-                       if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
-                               $keyword = 'RFC';
-                               $urlmsg = 'rfcurl';
-                               $id = $m[1];
-                       } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
-                               $keyword = 'PMID';
-                               $urlmsg = 'pubmedurl';
-                               $id = $m[1];
-                       } else {
-                               throw new MWException( __METHOD__.': unrecognised match type "' .
-                                       substr($m[0], 0, 20 ) . '"' );
-                       }
-
-                       $url = wfMsg( $urlmsg, $id);
-                       $sk = $this->mOptions->getSkin();
-                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
-                       $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
-               }
-               return $text;
-       }
-
-       /**
-        * Parse headers and return html
-        *
-        * @private
-        */
-       function doHeadings( $text ) {
-               $fname = 'Parser::doHeadings';
-               wfProfileIn( $fname );
-               for ( $i = 6; $i >= 1; --$i ) {
-                       $h = str_repeat( '=', $i );
-                       $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m",
-                         "<h{$i}>\\1</h{$i}>\\2", $text );
-               }
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * Replace single quotes with HTML markup
-        * @private
-        * @return string the altered text
-        */
-       function doAllQuotes( $text ) {
-               $fname = 'Parser::doAllQuotes';
-               wfProfileIn( $fname );
-               $outtext = '';
-               $lines = explode( "\n", $text );
-               foreach ( $lines as $line ) {
-                       $outtext .= $this->doQuotes ( $line ) . "\n";
-               }
-               $outtext = substr($outtext, 0,-1);
-               wfProfileOut( $fname );
-               return $outtext;
-       }
-
-       /**
-        * Helper function for doAllQuotes()
-        */
-       public function doQuotes( $text ) {
-               $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-               if ( count( $arr ) == 1 )
-                       return $text;
-               else
-               {
-                       # First, do some preliminary work. This may shift some apostrophes from
-                       # being mark-up to being text. It also counts the number of occurrences
-                       # of bold and italics mark-ups.
-                       $i = 0;
-                       $numbold = 0;
-                       $numitalics = 0;
-                       foreach ( $arr as $r )
-                       {
-                               if ( ( $i % 2 ) == 1 )
-                               {
-                                       # If there are ever four apostrophes, assume the first is supposed to
-                                       # be text, and the remaining three constitute mark-up for bold text.
-                                       if ( strlen( $arr[$i] ) == 4 )
-                                       {
-                                               $arr[$i-1] .= "'";
-                                               $arr[$i] = "'''";
-                                       }
-                                       # If there are more than 5 apostrophes in a row, assume they're all
-                                       # text except for the last 5.
-                                       else if ( strlen( $arr[$i] ) > 5 )
-                                       {
-                                               $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
-                                               $arr[$i] = "'''''";
-                                       }
-                                       # Count the number of occurrences of bold and italics mark-ups.
-                                       # We are not counting sequences of five apostrophes.
-                                       if ( strlen( $arr[$i] ) == 2 )      { $numitalics++;             }
-                                       else if ( strlen( $arr[$i] ) == 3 ) { $numbold++;                }
-                                       else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
-                               }
-                               $i++;
-                       }
-
-                       # If there is an odd number of both bold and italics, it is likely
-                       # that one of the bold ones was meant to be an apostrophe followed
-                       # by italics. Which one we cannot know for certain, but it is more
-                       # likely to be one that has a single-letter word before it.
-                       if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
-                       {
-                               $i = 0;
-                               $firstsingleletterword = -1;
-                               $firstmultiletterword = -1;
-                               $firstspace = -1;
-                               foreach ( $arr as $r )
-                               {
-                                       if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
-                                       {
-                                               $x1 = substr ($arr[$i-1], -1);
-                                               $x2 = substr ($arr[$i-1], -2, 1);
-                                               if ($x1 == ' ') {
-                                                       if ($firstspace == -1) $firstspace = $i;
-                                               } else if ($x2 == ' ') {
-                                                       if ($firstsingleletterword == -1) $firstsingleletterword = $i;
-                                               } else {
-                                                       if ($firstmultiletterword == -1) $firstmultiletterword = $i;
-                                               }
-                                       }
-                                       $i++;
-                               }
-
-                               # If there is a single-letter word, use it!
-                               if ($firstsingleletterword > -1)
-                               {
-                                       $arr [ $firstsingleletterword ] = "''";
-                                       $arr [ $firstsingleletterword-1 ] .= "'";
-                               }
-                               # If not, but there's a multi-letter word, use that one.
-                               else if ($firstmultiletterword > -1)
-                               {
-                                       $arr [ $firstmultiletterword ] = "''";
-                                       $arr [ $firstmultiletterword-1 ] .= "'";
-                               }
-                               # ... otherwise use the first one that has neither.
-                               # (notice that it is possible for all three to be -1 if, for example,
-                               # there is only one pentuple-apostrophe in the line)
-                               else if ($firstspace > -1)
-                               {
-                                       $arr [ $firstspace ] = "''";
-                                       $arr [ $firstspace-1 ] .= "'";
-                               }
-                       }
-
-                       # Now let's actually convert our apostrophic mush to HTML!
-                       $output = '';
-                       $buffer = '';
-                       $state = '';
-                       $i = 0;
-                       foreach ($arr as $r)
-                       {
-                               if (($i % 2) == 0)
-                               {
-                                       if ($state == 'both')
-                                               $buffer .= $r;
-                                       else
-                                               $output .= $r;
-                               }
-                               else
-                               {
-                                       if (strlen ($r) == 2)
-                                       {
-                                               if ($state == 'i')
-                                               { $output .= '</i>'; $state = ''; }
-                                               else if ($state == 'bi')
-                                               { $output .= '</i>'; $state = 'b'; }
-                                               else if ($state == 'ib')
-                                               { $output .= '</b></i><b>'; $state = 'b'; }
-                                               else if ($state == 'both')
-                                               { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
-                                               else # $state can be 'b' or ''
-                                               { $output .= '<i>'; $state .= 'i'; }
-                                       }
-                                       else if (strlen ($r) == 3)
-                                       {
-                                               if ($state == 'b')
-                                               { $output .= '</b>'; $state = ''; }
-                                               else if ($state == 'bi')
-                                               { $output .= '</i></b><i>'; $state = 'i'; }
-                                               else if ($state == 'ib')
-                                               { $output .= '</b>'; $state = 'i'; }
-                                               else if ($state == 'both')
-                                               { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
-                                               else # $state can be 'i' or ''
-                                               { $output .= '<b>'; $state .= 'b'; }
-                                       }
-                                       else if (strlen ($r) == 5)
-                                       {
-                                               if ($state == 'b')
-                                               { $output .= '</b><i>'; $state = 'i'; }
-                                               else if ($state == 'i')
-                                               { $output .= '</i><b>'; $state = 'b'; }
-                                               else if ($state == 'bi')
-                                               { $output .= '</i></b>'; $state = ''; }
-                                               else if ($state == 'ib')
-                                               { $output .= '</b></i>'; $state = ''; }
-                                               else if ($state == 'both')
-                                               { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
-                                               else # ($state == '')
-                                               { $buffer = ''; $state = 'both'; }
-                                       }
-                               }
-                               $i++;
-                       }
-                       # Now close all remaining tags.  Notice that the order is important.
-                       if ($state == 'b' || $state == 'ib')
-                               $output .= '</b>';
-                       if ($state == 'i' || $state == 'bi' || $state == 'ib')
-                               $output .= '</i>';
-                       if ($state == 'bi')
-                               $output .= '</b>';
-                       # There might be lonely ''''', so make sure we have a buffer
-                       if ($state == 'both' && $buffer)
-                               $output .= '<b><i>'.$buffer.'</i></b>';
-                       return $output;
-               }
-       }
-
-       /**
-        * Replace external links
-        *
-        * Note: this is all very hackish and the order of execution matters a lot.
-        * Make sure to run maintenance/parserTests.php if you change this code.
-        *
-        * @private
-        */
-       function replaceExternalLinks( $text ) {
-               global $wgContLang;
-               $fname = 'Parser::replaceExternalLinks';
-               wfProfileIn( $fname );
-
-               $sk = $this->mOptions->getSkin();
-
-               $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-
-               $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
-
-               $i = 0;
-               while ( $i<count( $bits ) ) {
-                       $url = $bits[$i++];
-                       $protocol = $bits[$i++];
-                       $text = $bits[$i++];
-                       $trail = $bits[$i++];
-
-                       # The characters '<' and '>' (which were escaped by
-                       # removeHTMLtags()) should not be included in
-                       # URLs, per RFC 2396.
-                       $m2 = array();
-                       if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                               $text = substr($url, $m2[0][1]) . ' ' . $text;
-                               $url = substr($url, 0, $m2[0][1]);
-                       }
-
-                       # If the link text is an image URL, replace it with an <img> tag
-                       # This happened by accident in the original parser, but some people used it extensively
-                       $img = $this->maybeMakeExternalImage( $text );
-                       if ( $img !== false ) {
-                               $text = $img;
-                       }
-
-                       $dtrail = '';
-
-                       # Set linktype for CSS - if URL==text, link is essentially free
-                       $linktype = ($text == $url) ? 'free' : 'text';
-
-                       # No link text, e.g. [http://domain.tld/some.link]
-                       if ( $text == '' ) {
-                               # Autonumber if allowed. See bug #5918
-                               if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
-                                       $text = '[' . ++$this->mAutonumber . ']';
-                                       $linktype = 'autonumber';
-                               } else {
-                                       # Otherwise just use the URL
-                                       $text = htmlspecialchars( $url );
-                                       $linktype = 'free';
-                               }
-                       } else {
-                               # Have link text, e.g. [http://domain.tld/some.link text]s
-                               # Check for trail
-                               list( $dtrail, $trail ) = Linker::splitTrail( $trail );
-                       }
-
-                       $text = $wgContLang->markNoConversion($text);
-
-                       $url = Sanitizer::cleanUrl( $url );
-
-                       # Process the trail (i.e. everything after this link up until start of the next link),
-                       # replacing any non-bracketed links
-                       $trail = $this->replaceFreeExternalLinks( $trail );
-
-                       # Use the encoded URL
-                       # This means that users can paste URLs directly into the text
-                       # Funny characters like &ouml; aren't valid in URLs anyway
-                       # This was changed in August 2004
-                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
-
-                       # Register link in the output object.
-                       # Replace unnecessary URL escape codes with the referenced character
-                       # This prevents spammers from hiding links from the filters
-                       $pasteurized = self::replaceUnusualEscapes( $url );
-                       $this->mOutput->addExternalLink( $pasteurized );
-               }
-
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Replace anything that looks like a URL with a link
-        * @private
-        */
-       function replaceFreeExternalLinks( $text ) {
-               global $wgContLang;
-               $fname = 'Parser::replaceFreeExternalLinks';
-               wfProfileIn( $fname );
-
-               $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-               $s = array_shift( $bits );
-               $i = 0;
-
-               $sk = $this->mOptions->getSkin();
-
-               while ( $i < count( $bits ) ){
-                       $protocol = $bits[$i++];
-                       $remainder = $bits[$i++];
-
-                       $m = array();
-                       if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
-                               # Found some characters after the protocol that look promising
-                               $url = $protocol . $m[1];
-                               $trail = $m[2];
-
-                               # special case: handle urls as url args:
-                               # http://www.example.com/foo?=http://www.example.com/bar
-                               if(strlen($trail) == 0 &&
-                                       isset($bits[$i]) &&
-                                       preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
-                                       preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
-                               {
-                                       # add protocol, arg
-                                       $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
-                                       $i += 2;
-                                       $trail = $m[2];
-                               }
-
-                               # The characters '<' and '>' (which were escaped by
-                               # removeHTMLtags()) should not be included in
-                               # URLs, per RFC 2396.
-                               $m2 = array();
-                               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                                       $trail = substr($url, $m2[0][1]) . $trail;
-                                       $url = substr($url, 0, $m2[0][1]);
-                               }
-
-                               # Move trailing punctuation to $trail
-                               $sep = ',;\.:!?';
-                               # If there is no left bracket, then consider right brackets fair game too
-                               if ( strpos( $url, '(' ) === false ) {
-                                       $sep .= ')';
-                               }
-
-                               $numSepChars = strspn( strrev( $url ), $sep );
-                               if ( $numSepChars ) {
-                                       $trail = substr( $url, -$numSepChars ) . $trail;
-                                       $url = substr( $url, 0, -$numSepChars );
-                               }
-
-                               $url = Sanitizer::cleanUrl( $url );
-
-                               # Is this an external image?
-                               $text = $this->maybeMakeExternalImage( $url );
-                               if ( $text === false ) {
-                                       # Not an image, make a link
-                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
-                                       # Register it in the output object...
-                                       # Replace unnecessary URL escape codes with their equivalent characters
-                                       $pasteurized = self::replaceUnusualEscapes( $url );
-                                       $this->mOutput->addExternalLink( $pasteurized );
-                               }
-                               $s .= $text . $trail;
-                       } else {
-                               $s .= $protocol . $remainder;
-                       }
-               }
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Replace unusual URL escape codes with their equivalent characters
-        * @param string
-        * @return string
-        * @static
-        * @todo  This can merge genuinely required bits in the path or query string,
-        *        breaking legit URLs. A proper fix would treat the various parts of
-        *        the URL differently; as a workaround, just use the output for
-        *        statistical records, not for actual linking/output.
-        */
-       static function replaceUnusualEscapes( $url ) {
-               return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
-                       array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
-       }
-
-       /**
-        * Callback function used in replaceUnusualEscapes().
-        * Replaces unusual URL escape codes with their equivalent character
-        * @static
-        * @private
-        */
-       private static function replaceUnusualEscapesCallback( $matches ) {
-               $char = urldecode( $matches[0] );
-               $ord = ord( $char );
-               // Is it an unsafe or HTTP reserved character according to RFC 1738?
-               if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
-                       // No, shouldn't be escaped
-                       return $char;
-               } else {
-                       // Yes, leave it escaped
-                       return $matches[0];
-               }
-       }
-
-       /**
-        * make an image if it's allowed, either through the global
-        * option or through the exception
-        * @private
-        */
-       function maybeMakeExternalImage( $url ) {
-               $sk = $this->mOptions->getSkin();
-               $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
-               $imagesexception = !empty($imagesfrom);
-               $text = false;
-               if ( $this->mOptions->getAllowExternalImages()
-                    || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
-                       if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
-                               # Image found
-                               $text = $sk->makeExternalImage( htmlspecialchars( $url ) );
-                       }
-               }
-               return $text;
-       }
-
-       /**
-        * Process [[ ]] wikilinks
-        *
-        * @private
-        */
-       function replaceInternalLinks( $s ) {
-               global $wgContLang;
-               static $fname = 'Parser::replaceInternalLinks' ;
-
-               wfProfileIn( $fname );
-
-               wfProfileIn( $fname.'-setup' );
-               static $tc = FALSE;
-               # the % is needed to support urlencoded titles as well
-               if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
-
-               $sk = $this->mOptions->getSkin();
-
-               #split the entire text string on occurences of [[
-               $a = explode( '[[', ' ' . $s );
-               #get the first element (all text up to first [[), and remove the space we added
-               $s = array_shift( $a );
-               $s = substr( $s, 1 );
-
-               # Match a link having the form [[namespace:link|alternate]]trail
-               static $e1 = FALSE;
-               if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
-               # Match cases where there is no "]]", which might still be images
-               static $e1_img = FALSE;
-               if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
-               # Match the end of a line for a word that's not followed by whitespace,
-               # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
-               $e2 = wfMsgForContent( 'linkprefix' );
-
-               $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
-               if( is_null( $this->mTitle ) ) {
-                       throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
-               }
-               $nottalk = !$this->mTitle->isTalkPage();
-
-               if ( $useLinkPrefixExtension ) {
-                       $m = array();
-                       if ( preg_match( $e2, $s, $m ) ) {
-                               $first_prefix = $m[2];
-                       } else {
-                               $first_prefix = false;
-                       }
-               } else {
-                       $prefix = '';
-               }
-
-               if($wgContLang->hasVariants()) {
-                       $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
-               } else {
-                       $selflink = array($this->mTitle->getPrefixedText());
-               }
-               $useSubpages = $this->areSubpagesAllowed();
-               wfProfileOut( $fname.'-setup' );
-
-               # Loop for each link
-               for ($k = 0; isset( $a[$k] ); $k++) {
-                       $line = $a[$k];
-                       if ( $useLinkPrefixExtension ) {
-                               wfProfileIn( $fname.'-prefixhandling' );
-                               if ( preg_match( $e2, $s, $m ) ) {
-                                       $prefix = $m[2];
-                                       $s = $m[1];
-                               } else {
-                                       $prefix='';
-                               }
-                               # first link
-                               if($first_prefix) {
-                                       $prefix = $first_prefix;
-                                       $first_prefix = false;
-                               }
-                               wfProfileOut( $fname.'-prefixhandling' );
-                       }
-
-                       $might_be_img = false;
-
-                       wfProfileIn( "$fname-e1" );
-                       if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
-                               $text = $m[2];
-                               # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
-                               # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
-                               # the real problem is with the $e1 regex
-                               # See bug 1300.
-                               #
-                               # Still some problems for cases where the ] is meant to be outside punctuation,
-                               # and no image is in sight. See bug 2095.
-                               #
-                               if( $text !== '' &&
-                                       substr( $m[3], 0, 1 ) === ']' &&
-                                       strpos($text, '[') !== false
-                               )
-                               {
-                                       $text .= ']'; # so that replaceExternalLinks($text) works later
-                                       $m[3] = substr( $m[3], 1 );
-                               }
-                               # fix up urlencoded title texts
-                               if( strpos( $m[1], '%' ) !== false ) {
-                                       # Should anchors '#' also be rejected?
-                                       $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
-                               }
-                               $trail = $m[3];
-                       } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
-                               $might_be_img = true;
-                               $text = $m[2];
-                               if ( strpos( $m[1], '%' ) !== false ) {
-                                       $m[1] = urldecode($m[1]);
-                               }
-                               $trail = "";
-                       } else { # Invalid form; output directly
-                               $s .= $prefix . '[[' . $line ;
-                               wfProfileOut( "$fname-e1" );
-                               continue;
-                       }
-                       wfProfileOut( "$fname-e1" );
-                       wfProfileIn( "$fname-misc" );
-
-                       # Don't allow internal links to pages containing
-                       # PROTO: where PROTO is a valid URL protocol; these
-                       # should be external links.
-                       if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
-                               $s .= $prefix . '[[' . $line ;
-                               continue;
-                       }
-
-                       # Make subpage if necessary
-                       if( $useSubpages ) {
-                               $link = $this->maybeDoSubpageLink( $m[1], $text );
-                       } else {
-                               $link = $m[1];
-                       }
-
-                       $noforce = (substr($m[1], 0, 1) != ':');
-                       if (!$noforce) {
-                               # Strip off leading ':'
-                               $link = substr($link, 1);
-                       }
-
-                       wfProfileOut( "$fname-misc" );
-                       wfProfileIn( "$fname-title" );
-                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
-                       if( !$nt ) {
-                               $s .= $prefix . '[[' . $line;
-                               wfProfileOut( "$fname-title" );
-                               continue;
-                       }
-
-                       $ns = $nt->getNamespace();
-                       $iw = $nt->getInterWiki();
-                       wfProfileOut( "$fname-title" );
-
-                       if ($might_be_img) { # if this is actually an invalid link
-                               wfProfileIn( "$fname-might_be_img" );
-                               if ($ns == NS_IMAGE && $noforce) { #but might be an image
-                                       $found = false;
-                                       while (isset ($a[$k+1]) ) {
-                                               #look at the next 'line' to see if we can close it there
-                                               $spliced = array_splice( $a, $k + 1, 1 );
-                                               $next_line = array_shift( $spliced );
-                                               $m = explode( ']]', $next_line, 3 );
-                                               if ( count( $m ) == 3 ) {
-                                                       # the first ]] closes the inner link, the second the image
-                                                       $found = true;
-                                                       $text .= "[[{$m[0]}]]{$m[1]}";
-                                                       $trail = $m[2];
-                                                       break;
-                                               } elseif ( count( $m ) == 2 ) {
-                                                       #if there's exactly one ]] that's fine, we'll keep looking
-                                                       $text .= "[[{$m[0]}]]{$m[1]}";
-                                               } else {
-                                                       #if $next_line is invalid too, we need look no further
-                                                       $text .= '[[' . $next_line;
-                                                       break;
-                                               }
-                                       }
-                                       if ( !$found ) {
-                                               # we couldn't find the end of this imageLink, so output it raw
-                                               #but don't ignore what might be perfectly normal links in the text we've examined
-                                               $text = $this->replaceInternalLinks($text);
-                                               $s .= "{$prefix}[[$link|$text";
-                                               # note: no $trail, because without an end, there *is* no trail
-                                               wfProfileOut( "$fname-might_be_img" );
-                                               continue;
-                                       }
-                               } else { #it's not an image, so output it raw
-                                       $s .= "{$prefix}[[$link|$text";
-                                       # note: no $trail, because without an end, there *is* no trail
-                                       wfProfileOut( "$fname-might_be_img" );
-                                       continue;
-                               }
-                               wfProfileOut( "$fname-might_be_img" );
-                       }
-
-                       $wasblank = ( '' == $text );
-                       if( $wasblank ) $text = $link;
-
-                       # Link not escaped by : , create the various objects
-                       if( $noforce ) {
-
-                               # Interwikis
-                               wfProfileIn( "$fname-interwiki" );
-                               if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
-                                       $this->mOutput->addLanguageLink( $nt->getFullText() );
-                                       $s = rtrim($s . $prefix);
-                                       $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
-                                       wfProfileOut( "$fname-interwiki" );
-                                       continue;
-                               }
-                               wfProfileOut( "$fname-interwiki" );
-
-                               if ( $ns == NS_IMAGE ) {
-                                       wfProfileIn( "$fname-image" );
-                                       if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
-                                               # recursively parse links inside the image caption
-                                               # actually, this will parse them in any other parameters, too,
-                                               # but it might be hard to fix that, and it doesn't matter ATM
-                                               $text = $this->replaceExternalLinks($text);
-                                               $text = $this->replaceInternalLinks($text);
-
-                                               # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
-                                               $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
-                                               $this->mOutput->addImage( $nt->getDBkey() );
-
-                                               wfProfileOut( "$fname-image" );
-                                               continue;
-                                       } else {
-                                               # We still need to record the image's presence on the page
-                                               $this->mOutput->addImage( $nt->getDBkey() );
-                                       }
-                                       wfProfileOut( "$fname-image" );
-
-                               }
-
-                               if ( $ns == NS_CATEGORY ) {
-                                       wfProfileIn( "$fname-category" );
-                                       $s = rtrim($s . "\n"); # bug 87
-
-                                       if ( $wasblank ) {
-                                               $sortkey = $this->getDefaultSort();
-                                       } else {
-                                               $sortkey = $text;
-                                       }
-                                       $sortkey = Sanitizer::decodeCharReferences( $sortkey );
-                                       $sortkey = str_replace( "\n", '', $sortkey );
-                                       $sortkey = $wgContLang->convertCategoryKey( $sortkey );
-                                       $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
-
-                                       /**
-                                        * Strip the whitespace Category links produce, see bug 87
-                                        * @todo We might want to use trim($tmp, "\n") here.
-                                        */
-                                       $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
-
-                                       wfProfileOut( "$fname-category" );
-                                       continue;
-                               }
-                       }
-
-                       # Self-link checking
-                       if( $nt->getFragment() === '' ) {
-                               if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
-                                       $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
-                                       continue;
-                               }
-                       }
-
-                       # Special and Media are pseudo-namespaces; no pages actually exist in them
-                       if( $ns == NS_MEDIA ) {
-                               # Give extensions a chance to select the file revision for us
-                               $skip = $time = false;
-                               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
-                               if ( $skip ) {
-                                       $link = $sk->makeLinkObj( $nt );
-                               } else {
-                                       $link = $sk->makeMediaLinkObj( $nt, $text, $time );
-                               }
-                               # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
-                               $s .= $prefix . $this->armorLinks( $link ) . $trail;
-                               $this->mOutput->addImage( $nt->getDBkey() );
-                               continue;
-                       } elseif( $ns == NS_SPECIAL ) {
-                               if( SpecialPage::exists( $nt->getDBkey() ) ) {
-                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
-                               } else {
-                                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
-                               }
-                               continue;
-                       } elseif( $ns == NS_IMAGE ) {
-                               $img = wfFindFile( $nt );
-                               if( $img ) {
-                                       // Force a blue link if the file exists; may be a remote
-                                       // upload on the shared repository, and we want to see its
-                                       // auto-generated page.
-                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
-                                       $this->mOutput->addLink( $nt );
-                                       continue;
-                               }
-                       }
-                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
-               }
-               wfProfileOut( $fname );
-               return $s;
-       }
-
-       /**
-        * Make a link placeholder. The text returned can be later resolved to a real link with
-        * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
-        * parsing of interwiki links, and secondly to allow all existence checks and
-        * article length checks (for stub links) to be bundled into a single query.
-        *
-        */
-       function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-               wfProfileIn( __METHOD__ );
-               if ( ! is_object($nt) ) {
-                       # Fail gracefully
-                       $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
-               } else {
-                       # Separate the link trail from the rest of the link
-                       list( $inside, $trail ) = Linker::splitTrail( $trail );
-
-                       if ( $nt->isExternal() ) {
-                               $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
-                               $this->mInterwikiLinkHolders['titles'][] = $nt;
-                               $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
-                       } else {
-                               $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
-                               $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
-                               $this->mLinkHolders['queries'][] = $query;
-                               $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
-                               $this->mLinkHolders['titles'][] = $nt;
-
-                               $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-               return $retVal;
-       }
-
-       /**
-        * Render a forced-blue link inline; protect against double expansion of
-        * URLs if we're in a mode that prepends full URL prefixes to internal links.
-        * Since this little disaster has to split off the trail text to avoid
-        * breaking URLs in the following text without breaking trails on the
-        * wiki links, it's been made into a horrible function.
-        *
-        * @param Title $nt
-        * @param string $text
-        * @param string $query
-        * @param string $trail
-        * @param string $prefix
-        * @return string HTML-wikitext mix oh yuck
-        */
-       function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-               list( $inside, $trail ) = Linker::splitTrail( $trail );
-               $sk = $this->mOptions->getSkin();
-               $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
-               return $this->armorLinks( $link ) . $trail;
-       }
-
-       /**
-        * Insert a NOPARSE hacky thing into any inline links in a chunk that's
-        * going to go through further parsing steps before inline URL expansion.
-        *
-        * In particular this is important when using action=render, which causes
-        * full URLs to be included.
-        *
-        * Oh man I hate our multi-layer parser!
-        *
-        * @param string more-or-less HTML
-        * @return string less-or-more HTML with NOPARSE bits
-        */
-       function armorLinks( $text ) {
-               return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
-                       "{$this->mUniqPrefix}NOPARSE$1", $text );
-       }
-
-       /**
-        * Return true if subpage links should be expanded on this page.
-        * @return bool
-        */
-       function areSubpagesAllowed() {
-               # Some namespaces don't allow subpages
-               global $wgNamespacesWithSubpages;
-               return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]);
-       }
-
-       /**
-        * Handle link to subpage if necessary
-        * @param string $target the source of the link
-        * @param string &$text the link text, modified as necessary
-        * @return string the full name of the link
-        * @private
-        */
-       function maybeDoSubpageLink($target, &$text) {
-               # Valid link forms:
-               # Foobar -- normal
-               # :Foobar -- override special treatment of prefix (images, language links)
-               # /Foobar -- convert to CurrentPage/Foobar
-               # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
-               # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
-               # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
-
-               $fname = 'Parser::maybeDoSubpageLink';
-               wfProfileIn( $fname );
-               $ret = $target; # default return value is no change
-
-               # Some namespaces don't allow subpages,
-               # so only perform processing if subpages are allowed
-               if( $this->areSubpagesAllowed() ) {
-                       $hash = strpos( $target, '#' );
-                       if( $hash !== false ) {
-                               $suffix = substr( $target, $hash );
-                               $target = substr( $target, 0, $hash );
-                       } else {
-                               $suffix = '';
-                       }
-                       # bug 7425
-                       $target = trim( $target );
-                       # Look at the first character
-                       if( $target != '' && $target{0} == '/' ) {
-                               # / at end means we don't want the slash to be shown
-                               $m = array();
-                               $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
-                               if( $trailingSlashes ) {
-                                       $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
-                               } else {
-                                       $noslash = substr( $target, 1 );
-                               }
-
-                               $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
-                               if( '' === $text ) {
-                                       $text = $target . $suffix;
-                               } # this might be changed for ugliness reasons
-                       } else {
-                               # check for .. subpage backlinks
-                               $dotdotcount = 0;
-                               $nodotdot = $target;
-                               while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
-                                       ++$dotdotcount;
-                                       $nodotdot = substr( $nodotdot, 3 );
-                               }
-                               if($dotdotcount > 0) {
-                                       $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
-                                       if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
-                                               $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
-                                               # / at the end means don't show full path
-                                               if( substr( $nodotdot, -1, 1 ) == '/' ) {
-                                                       $nodotdot = substr( $nodotdot, 0, -1 );
-                                                       if( '' === $text ) {
-                                                               $text = $nodotdot . $suffix;
-                                                       }
-                                               }
-                                               $nodotdot = trim( $nodotdot );
-                                               if( $nodotdot != '' ) {
-                                                       $ret .= '/' . $nodotdot;
-                                               }
-                                               $ret .= $suffix;
-                                       }
-                               }
-                       }
-               }
-
-               wfProfileOut( $fname );
-               return $ret;
-       }
-
-       /**#@+
-        * Used by doBlockLevels()
-        * @private
-        */
-       /* private */ function closeParagraph() {
-               $result = '';
-               if ( '' != $this->mLastSection ) {
-                       $result = '</' . $this->mLastSection  . ">\n";
-               }
-               $this->mInPre = false;
-               $this->mLastSection = '';
-               return $result;
-       }
-       # getCommon() returns the length of the longest common substring
-       # of both arguments, starting at the beginning of both.
-       #
-       /* private */ function getCommon( $st1, $st2 ) {
-               $fl = strlen( $st1 );
-               $shorter = strlen( $st2 );
-               if ( $fl < $shorter ) { $shorter = $fl; }
-
-               for ( $i = 0; $i < $shorter; ++$i ) {
-                       if ( $st1{$i} != $st2{$i} ) { break; }
-               }
-               return $i;
-       }
-       # These next three functions open, continue, and close the list
-       # element appropriate to the prefix character passed into them.
-       #
-       /* private */ function openList( $char ) {
-               $result = $this->closeParagraph();
-
-               if ( '*' == $char ) { $result .= '<ul><li>'; }
-               else if ( '#' == $char ) { $result .= '<ol><li>'; }
-               else if ( ':' == $char ) { $result .= '<dl><dd>'; }
-               else if ( ';' == $char ) {
-                       $result .= '<dl><dt>';
-                       $this->mDTopen = true;
-               }
-               else { $result = '<!-- ERR 1 -->'; }
-
-               return $result;
-       }
-
-       /* private */ function nextItem( $char ) {
-               if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
-               else if ( ':' == $char || ';' == $char ) {
-                       $close = '</dd>';
-                       if ( $this->mDTopen ) { $close = '</dt>'; }
-                       if ( ';' == $char ) {
-                               $this->mDTopen = true;
-                               return $close . '<dt>';
-                       } else {
-                               $this->mDTopen = false;
-                               return $close . '<dd>';
-                       }
-               }
-               return '<!-- ERR 2 -->';
-       }
-
-       /* private */ function closeList( $char ) {
-               if ( '*' == $char ) { $text = '</li></ul>'; }
-               else if ( '#' == $char ) { $text = '</li></ol>'; }
-               else if ( ':' == $char ) {
-                       if ( $this->mDTopen ) {
-                               $this->mDTopen = false;
-                               $text = '</dt></dl>';
-                       } else {
-                               $text = '</dd></dl>';
-                       }
-               }
-               else {  return '<!-- ERR 3 -->'; }
-               return $text."\n";
-       }
-       /**#@-*/
-
-       /**
-        * Make lists from lines starting with ':', '*', '#', etc.
-        *
-        * @private
-        * @return string the lists rendered as HTML
-        */
-       function doBlockLevels( $text, $linestart ) {
-               $fname = 'Parser::doBlockLevels';
-               wfProfileIn( $fname );
-
-               # Parsing through the text line by line.  The main thing
-               # happening here is handling of block-level elements p, pre,
-               # and making lists from lines starting with * # : etc.
-               #
-               $textLines = explode( "\n", $text );
-
-               $lastPrefix = $output = '';
-               $this->mDTopen = $inBlockElem = false;
-               $prefixLength = 0;
-               $paragraphStack = false;
-
-               if ( !$linestart ) {
-                       $output .= array_shift( $textLines );
-               }
-               foreach ( $textLines as $oLine ) {
-                       $lastPrefixLength = strlen( $lastPrefix );
-                       $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
-                       $preOpenMatch = preg_match('/<pre/i', $oLine );
-                       if ( !$this->mInPre ) {
-                               # Multiple prefixes may abut each other for nested lists.
-                               $prefixLength = strspn( $oLine, '*#:;' );
-                               $pref = substr( $oLine, 0, $prefixLength );
-
-                               # eh?
-                               $pref2 = str_replace( ';', ':', $pref );
-                               $t = substr( $oLine, $prefixLength );
-                               $this->mInPre = !empty($preOpenMatch);
-                       } else {
-                               # Don't interpret any other prefixes in preformatted text
-                               $prefixLength = 0;
-                               $pref = $pref2 = '';
-                               $t = $oLine;
-                       }
-
-                       # List generation
-                       if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
-                               # Same as the last item, so no need to deal with nesting or opening stuff
-                               $output .= $this->nextItem( substr( $pref, -1 ) );
-                               $paragraphStack = false;
-
-                               if ( substr( $pref, -1 ) == ';') {
-                                       # The one nasty exception: definition lists work like this:
-                                       # ; title : definition text
-                                       # So we check for : in the remainder text to split up the
-                                       # title and definition, without b0rking links.
-                                       $term = $t2 = '';
-                                       if ($this->findColonNoLinks($t, $term, $t2) !== false) {
-                                               $t = $t2;
-                                               $output .= $term . $this->nextItem( ':' );
-                                       }
-                               }
-                       } elseif( $prefixLength || $lastPrefixLength ) {
-                               # Either open or close a level...
-                               $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
-                               $paragraphStack = false;
-
-                               while( $commonPrefixLength < $lastPrefixLength ) {
-                                       $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
-                                       --$lastPrefixLength;
-                               }
-                               if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
-                                       $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
-                               }
-                               while ( $prefixLength > $commonPrefixLength ) {
-                                       $char = substr( $pref, $commonPrefixLength, 1 );
-                                       $output .= $this->openList( $char );
-
-                                       if ( ';' == $char ) {
-                                               # FIXME: This is dupe of code above
-                                               if ($this->findColonNoLinks($t, $term, $t2) !== false) {
-                                                       $t = $t2;
-                                                       $output .= $term . $this->nextItem( ':' );
-                                               }
-                                       }
-                                       ++$commonPrefixLength;
-                               }
-                               $lastPrefix = $pref2;
-                       }
-                       if( 0 == $prefixLength ) {
-                               wfProfileIn( "$fname-paragraph" );
-                               # No prefix (not in list)--go to paragraph mode
-                               // XXX: use a stack for nestable elements like span, table and div
-                               $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
-                               $closematch = preg_match(
-                                       '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
-                                       '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
-                               if ( $openmatch or $closematch ) {
-                                       $paragraphStack = false;
-                                       # TODO bug 5718: paragraph closed
-                                       $output .= $this->closeParagraph();
-                                       if ( $preOpenMatch and !$preCloseMatch ) {
-                                               $this->mInPre = true;
-                                       }
-                                       if ( $closematch ) {
-                                               $inBlockElem = false;
-                                       } else {
-                                               $inBlockElem = true;
-                                       }
-                               } else if ( !$inBlockElem && !$this->mInPre ) {
-                                       if ( '' != $t and ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
-                                               // pre
-                                               if ($this->mLastSection != 'pre') {
-                                                       $paragraphStack = false;
-                                                       $output .= $this->closeParagraph().'<pre>';
-                                                       $this->mLastSection = 'pre';
-                                               }
-                                               $t = substr( $t, 1 );
-                                       } else {
-                                               // paragraph
-                                               if ( '' == trim($t) ) {
-                                                       if ( $paragraphStack ) {
-                                                               $output .= $paragraphStack.'<br />';
-                                                               $paragraphStack = false;
-                                                               $this->mLastSection = 'p';
-                                                       } else {
-                                                               if ($this->mLastSection != 'p' ) {
-                                                                       $output .= $this->closeParagraph();
-                                                                       $this->mLastSection = '';
-                                                                       $paragraphStack = '<p>';
-                                                               } else {
-                                                                       $paragraphStack = '</p><p>';
-                                                               }
-                                                       }
-                                               } else {
-                                                       if ( $paragraphStack ) {
-                                                               $output .= $paragraphStack;
-                                                               $paragraphStack = false;
-                                                               $this->mLastSection = 'p';
-                                                       } else if ($this->mLastSection != 'p') {
-                                                               $output .= $this->closeParagraph().'<p>';
-                                                               $this->mLastSection = 'p';
-                                                       }
-                                               }
-                                       }
-                               }
-                               wfProfileOut( "$fname-paragraph" );
-                       }
-                       // somewhere above we forget to get out of pre block (bug 785)
-                       if($preCloseMatch && $this->mInPre) {
-                               $this->mInPre = false;
-                       }
-                       if ($paragraphStack === false) {
-                               $output .= $t."\n";
-                       }
-               }
-               while ( $prefixLength ) {
-                       $output .= $this->closeList( $pref2{$prefixLength-1} );
-                       --$prefixLength;
-               }
-               if ( '' != $this->mLastSection ) {
-                       $output .= '</' . $this->mLastSection . '>';
-                       $this->mLastSection = '';
-               }
-
-               wfProfileOut( $fname );
-               return $output;
-       }
-
-       /**
-        * Split up a string on ':', ignoring any occurences inside tags
-        * to prevent illegal overlapping.
-        * @param string $str the string to split
-        * @param string &$before set to everything before the ':'
-        * @param string &$after set to everything after the ':'
-        * return string the position of the ':', or false if none found
-        */
-       function findColonNoLinks($str, &$before, &$after) {
-               $fname = 'Parser::findColonNoLinks';
-               wfProfileIn( $fname );
-
-               $pos = strpos( $str, ':' );
-               if( $pos === false ) {
-                       // Nothing to find!
-                       wfProfileOut( $fname );
-                       return false;
-               }
-
-               $lt = strpos( $str, '<' );
-               if( $lt === false || $lt > $pos ) {
-                       // Easy; no tag nesting to worry about
-                       $before = substr( $str, 0, $pos );
-                       $after = substr( $str, $pos+1 );
-                       wfProfileOut( $fname );
-                       return $pos;
-               }
-
-               // Ugly state machine to walk through avoiding tags.
-               $state = self::COLON_STATE_TEXT;
-               $stack = 0;
-               $len = strlen( $str );
-               for( $i = 0; $i < $len; $i++ ) {
-                       $c = $str{$i};
-
-                       switch( $state ) {
-                       // (Using the number is a performance hack for common cases)
-                       case 0: // self::COLON_STATE_TEXT:
-                               switch( $c ) {
-                               case "<":
-                                       // Could be either a <start> tag or an </end> tag
-                                       $state = self::COLON_STATE_TAGSTART;
-                                       break;
-                               case ":":
-                                       if( $stack == 0 ) {
-                                               // We found it!
-                                               $before = substr( $str, 0, $i );
-                                               $after = substr( $str, $i + 1 );
-                                               wfProfileOut( $fname );
-                                               return $i;
-                                       }
-                                       // Embedded in a tag; don't break it.
-                                       break;
-                               default:
-                                       // Skip ahead looking for something interesting
-                                       $colon = strpos( $str, ':', $i );
-                                       if( $colon === false ) {
-                                               // Nothing else interesting
-                                               wfProfileOut( $fname );
-                                               return false;
-                                       }
-                                       $lt = strpos( $str, '<', $i );
-                                       if( $stack === 0 ) {
-                                               if( $lt === false || $colon < $lt ) {
-                                                       // We found it!
-                                                       $before = substr( $str, 0, $colon );
-                                                       $after = substr( $str, $colon + 1 );
-                                                       wfProfileOut( $fname );
-                                                       return $i;
-                                               }
-                                       }
-                                       if( $lt === false ) {
-                                               // Nothing else interesting to find; abort!
-                                               // We're nested, but there's no close tags left. Abort!
-                                               break 2;
-                                       }
-                                       // Skip ahead to next tag start
-                                       $i = $lt;
-                                       $state = self::COLON_STATE_TAGSTART;
-                               }
-                               break;
-                       case 1: // self::COLON_STATE_TAG:
-                               // In a <tag>
-                               switch( $c ) {
-                               case ">":
-                                       $stack++;
-                                       $state = self::COLON_STATE_TEXT;
-                                       break;
-                               case "/":
-                                       // Slash may be followed by >?
-                                       $state = self::COLON_STATE_TAGSLASH;
-                                       break;
-                               default:
-                                       // ignore
-                               }
-                               break;
-                       case 2: // self::COLON_STATE_TAGSTART:
-                               switch( $c ) {
-                               case "/":
-                                       $state = self::COLON_STATE_CLOSETAG;
-                                       break;
-                               case "!":
-                                       $state = self::COLON_STATE_COMMENT;
-                                       break;
-                               case ">":
-                                       // Illegal early close? This shouldn't happen D:
-                                       $state = self::COLON_STATE_TEXT;
-                                       break;
-                               default:
-                                       $state = self::COLON_STATE_TAG;
-                               }
-                               break;
-                       case 3: // self::COLON_STATE_CLOSETAG:
-                               // In a </tag>
-                               if( $c == ">" ) {
-                                       $stack--;
-                                       if( $stack < 0 ) {
-                                               wfDebug( "Invalid input in $fname; too many close tags\n" );
-                                               wfProfileOut( $fname );
-                                               return false;
-                                       }
-                                       $state = self::COLON_STATE_TEXT;
-                               }
-                               break;
-                       case self::COLON_STATE_TAGSLASH:
-                               if( $c == ">" ) {
-                                       // Yes, a self-closed tag <blah/>
-                                       $state = self::COLON_STATE_TEXT;
-                               } else {
-                                       // Probably we're jumping the gun, and this is an attribute
-                                       $state = self::COLON_STATE_TAG;
-                               }
-                               break;
-                       case 5: // self::COLON_STATE_COMMENT:
-                               if( $c == "-" ) {
-                                       $state = self::COLON_STATE_COMMENTDASH;
-                               }
-                               break;
-                       case self::COLON_STATE_COMMENTDASH:
-                               if( $c == "-" ) {
-                                       $state = self::COLON_STATE_COMMENTDASHDASH;
-                               } else {
-                                       $state = self::COLON_STATE_COMMENT;
-                               }
-                               break;
-                       case self::COLON_STATE_COMMENTDASHDASH:
-                               if( $c == ">" ) {
-                                       $state = self::COLON_STATE_TEXT;
-                               } else {
-                                       $state = self::COLON_STATE_COMMENT;
-                               }
-                               break;
-                       default:
-                               throw new MWException( "State machine error in $fname" );
-                       }
-               }
-               if( $stack > 0 ) {
-                       wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
-                       return false;
-               }
-               wfProfileOut( $fname );
-               return false;
-       }
-
-       /**
-        * Return value of a magic variable (like PAGENAME)
-        *
-        * @private
-        */
-       function getVariableValue( $index ) {
-               global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
-
-               /**
-                * Some of these require message or data lookups and can be
-                * expensive to check many times.
-                */
-               static $varCache = array();
-               if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) {
-                       if ( isset( $varCache[$index] ) ) {
-                               return $varCache[$index];
-                       }
-               }
-
-               $ts = time();
-               wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
-
-               # Use the time zone
-               global $wgLocaltimezone;
-               if ( isset( $wgLocaltimezone ) ) {
-                       $oldtz = getenv( 'TZ' );
-                       putenv( 'TZ='.$wgLocaltimezone );
-               }
-
-               wfSuppressWarnings(); // E_STRICT system time bitching
-               $localTimestamp = date( 'YmdHis', $ts );
-               $localMonth = date( 'm', $ts );
-               $localMonthName = date( 'n', $ts );
-               $localDay = date( 'j', $ts );
-               $localDay2 = date( 'd', $ts );
-               $localDayOfWeek = date( 'w', $ts );
-               $localWeek = date( 'W', $ts );
-               $localYear = date( 'Y', $ts );
-               $localHour = date( 'H', $ts );
-               if ( isset( $wgLocaltimezone ) ) {
-                       putenv( 'TZ='.$oldtz );
-               }
-               wfRestoreWarnings();
-
-               switch ( $index ) {
-                       case 'currentmonth':
-                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
-                       case 'currentmonthname':
-                               return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
-                       case 'currentmonthnamegen':
-                               return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
-                       case 'currentmonthabbrev':
-                               return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
-                       case 'currentday':
-                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
-                       case 'currentday2':
-                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
-                       case 'localmonth':
-                               return $varCache[$index] = $wgContLang->formatNum( $localMonth );
-                       case 'localmonthname':
-                               return $varCache[$index] = $wgContLang->getMonthName( $localMonthName );
-                       case 'localmonthnamegen':
-                               return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
-                       case 'localmonthabbrev':
-                               return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
-                       case 'localday':
-                               return $varCache[$index] = $wgContLang->formatNum( $localDay );
-                       case 'localday2':
-                               return $varCache[$index] = $wgContLang->formatNum( $localDay2 );
-                       case 'pagename':
-                               return wfEscapeWikiText( $this->mTitle->getText() );
-                       case 'pagenamee':
-                               return $this->mTitle->getPartialURL();
-                       case 'fullpagename':
-                               return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
-                       case 'fullpagenamee':
-                               return $this->mTitle->getPrefixedURL();
-                       case 'subpagename':
-                               return wfEscapeWikiText( $this->mTitle->getSubpageText() );
-                       case 'subpagenamee':
-                               return $this->mTitle->getSubpageUrlForm();
-                       case 'basepagename':
-                               return wfEscapeWikiText( $this->mTitle->getBaseText() );
-                       case 'basepagenamee':
-                               return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
-                       case 'talkpagename':
-                               if( $this->mTitle->canTalk() ) {
-                                       $talkPage = $this->mTitle->getTalkPage();
-                                       return wfEscapeWikiText( $talkPage->getPrefixedText() );
-                               } else {
-                                       return '';
-                               }
-                       case 'talkpagenamee':
-                               if( $this->mTitle->canTalk() ) {
-                                       $talkPage = $this->mTitle->getTalkPage();
-                                       return $talkPage->getPrefixedUrl();
-                               } else {
-                                       return '';
-                               }
-                       case 'subjectpagename':
-                               $subjPage = $this->mTitle->getSubjectPage();
-                               return wfEscapeWikiText( $subjPage->getPrefixedText() );
-                       case 'subjectpagenamee':
-                               $subjPage = $this->mTitle->getSubjectPage();
-                               return $subjPage->getPrefixedUrl();
-                       case 'revisionid':
-                               return $this->mRevisionId;
-                       case 'revisionday':
-                               return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
-                       case 'revisionday2':
-                               return substr( $this->getRevisionTimestamp(), 6, 2 );
-                       case 'revisionmonth':
-                               return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
-                       case 'revisionyear':
-                               return substr( $this->getRevisionTimestamp(), 0, 4 );
-                       case 'revisiontimestamp':
-                               return $this->getRevisionTimestamp();
-                       case 'namespace':
-                               return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
-                       case 'namespacee':
-                               return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
-                       case 'talkspace':
-                               return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
-                       case 'talkspacee':
-                               return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
-                       case 'subjectspace':
-                               return $this->mTitle->getSubjectNsText();
-                       case 'subjectspacee':
-                               return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
-                       case 'currentdayname':
-                               return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
-                       case 'currentyear':
-                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
-                       case 'currenttime':
-                               return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
-                       case 'currenthour':
-                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
-                       case 'currentweek':
-                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
-                               // int to remove the padding
-                               return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
-                       case 'currentdow':
-                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
-                       case 'localdayname':
-                               return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
-                       case 'localyear':
-                               return $varCache[$index] = $wgContLang->formatNum( $localYear, true );
-                       case 'localtime':
-                               return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false );
-                       case 'localhour':
-                               return $varCache[$index] = $wgContLang->formatNum( $localHour, true );
-                       case 'localweek':
-                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
-                               // int to remove the padding
-                               return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek );
-                       case 'localdow':
-                               return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
-                       case 'numberofarticles':
-                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
-                       case 'numberoffiles':
-                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() );
-                       case 'numberofusers':
-                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() );
-                       case 'numberofpages':
-                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
-                       case 'numberofadmins':
-                               return $varCache[$index]  = $wgContLang->formatNum( SiteStats::admins() );
-                       case 'numberofedits':
-                               return $varCache[$index]  = $wgContLang->formatNum( SiteStats::edits() );
-                       case 'currenttimestamp':
-                               return $varCache[$index] = wfTimestampNow();
-                       case 'localtimestamp':
-                               return $varCache[$index] = $localTimestamp;
-                       case 'currentversion':
-                               return $varCache[$index] = SpecialVersion::getVersion();
-                       case 'sitename':
-                               return $wgSitename;
-                       case 'server':
-                               return $wgServer;
-                       case 'servername':
-                               return $wgServerName;
-                       case 'scriptpath':
-                               return $wgScriptPath;
-                       case 'directionmark':
-                               return $wgContLang->getDirMark();
-                       case 'contentlanguage':
-                               global $wgContLanguageCode;
-                               return $wgContLanguageCode;
-                       default:
-                               $ret = null;
-                               if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
-                                       return $ret;
-                               else
-                                       return null;
-               }
-       }
-
-       /**
-        * initialise the magic variables (like CURRENTMONTHNAME)
-        *
-        * @private
-        */
-       function initialiseVariables() {
-               $fname = 'Parser::initialiseVariables';
-               wfProfileIn( $fname );
-               $variableIDs = MagicWord::getVariableIDs();
-
-               $this->mVariables = array();
-               foreach ( $variableIDs as $id ) {
-                       $mw =& MagicWord::get( $id );
-                       $mw->addToArray( $this->mVariables, $id );
-               }
-               wfProfileOut( $fname );
-       }
-
-       /**
-        * parse any parentheses in format ((title|part|part))
-        * and call callbacks to get a replacement text for any found piece
-        *
-        * @param string $text The text to parse
-        * @param array $callbacks rules in form:
-        *     '{' => array(                            # opening parentheses
-        *                                      'end' => '}',   # closing parentheses
-        *                                      'cb' => array(2 => callback,    # replacement callback to call if {{..}} is found
-        *                                                                3 => callback         # replacement callback to call if {{{..}}} is found
-        *                                                                )
-        *                                      )
-        *                                      'min' => 2,     # Minimum parenthesis count in cb
-        *                                      'max' => 3,     # Maximum parenthesis count in cb
-        * @private
-        */
-       function replace_callback ($text, $callbacks) {
-               wfProfileIn( __METHOD__ );
-               $openingBraceStack = array();   # this array will hold a stack of parentheses which are not closed yet
-               $lastOpeningBrace = -1;                 # last not closed parentheses
-
-               $validOpeningBraces = implode( '', array_keys( $callbacks ) );
-
-               $i = 0;
-               while ( $i < strlen( $text ) ) {
-                       # Find next opening brace, closing brace or pipe
-                       if ( $lastOpeningBrace == -1 ) {
-                               $currentClosing = '';
-                               $search = $validOpeningBraces;
-                       } else {
-                               $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd'];
-                               $search = $validOpeningBraces . '|' . $currentClosing;
-                       }
-                       $rule = null;
-                       $i += strcspn( $text, $search, $i );
-                       if ( $i < strlen( $text ) ) {
-                               if ( $text[$i] == '|' ) {
-                                       $found = 'pipe';
-                               } elseif ( $text[$i] == $currentClosing ) {
-                                       $found = 'close';
-                               } elseif ( isset( $callbacks[$text[$i]] ) ) {
-                                       $found = 'open';
-                                       $rule = $callbacks[$text[$i]];
-                               } else {
-                                       # Some versions of PHP have a strcspn which stops on null characters
-                                       # Ignore and continue
-                                       ++$i;
-                                       continue;
-                               }
-                       } else {
-                               # All done
-                               break;
-                       }
-
-                       if ( $found == 'open' ) {
-                               # found opening brace, let's add it to parentheses stack
-                               $piece = array('brace' => $text[$i],
-                                                          'braceEnd' => $rule['end'],
-                                                          'title' => '',
-                                                          'parts' => null);
-
-                               # count opening brace characters
-                               $piece['count'] = strspn( $text, $piece['brace'], $i );
-                               $piece['startAt'] = $piece['partStart'] = $i + $piece['count'];
-                               $i += $piece['count'];
-
-                               # we need to add to stack only if opening brace count is enough for one of the rules
-                               if ( $piece['count'] >= $rule['min'] ) {
-                                       $lastOpeningBrace ++;
-                                       $openingBraceStack[$lastOpeningBrace] = $piece;
-                               }
-                       } elseif ( $found == 'close' ) {
-                               # lets check if it is enough characters for closing brace
-                               $maxCount = $openingBraceStack[$lastOpeningBrace]['count'];
-                               $count = strspn( $text, $text[$i], $i, $maxCount );
-
-                               # check for maximum matching characters (if there are 5 closing
-                               # characters, we will probably need only 3 - depending on the rules)
-                               $matchingCount = 0;
-                               $matchingCallback = null;
-                               $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']];
-                               if ( $count > $cbType['max'] ) {
-                                       # The specified maximum exists in the callback array, unless the caller
-                                       # has made an error
-                                       $matchingCount = $cbType['max'];
-                               } else {
-                                       # Count is less than the maximum
-                                       # Skip any gaps in the callback array to find the true largest match
-                                       # Need to use array_key_exists not isset because the callback can be null
-                                       $matchingCount = $count;
-                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) {
-                                               --$matchingCount;
-                                       }
-                               }
-
-                               if ($matchingCount <= 0) {
-                                       $i += $count;
-                                       continue;
-                               }
-                               $matchingCallback = $cbType['cb'][$matchingCount];
-
-                               # let's set a title or last part (if '|' was found)
-                               if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
-                                       $openingBraceStack[$lastOpeningBrace]['title'] =
-                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
-                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-                               } else {
-                                       $openingBraceStack[$lastOpeningBrace]['parts'][] =
-                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
-                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-                               }
-
-                               $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
-                               $pieceEnd = $i + $matchingCount;
-
-                               if( is_callable( $matchingCallback ) ) {
-                                       $cbArgs = array (
-                                                                        'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
-                                                                        'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
-                                                                        'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
-                                                                        'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")),
-                                                                        );
-                                       # finally we can call a user callback and replace piece of text
-                                       $replaceWith = call_user_func( $matchingCallback, $cbArgs );
-                                       $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
-                                       $i = $pieceStart + strlen($replaceWith);
-                               } else {
-                                       # null value for callback means that parentheses should be parsed, but not replaced
-                                       $i += $matchingCount;
-                               }
-
-                               # reset last opening parentheses, but keep it in case there are unused characters
-                               $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
-                                                          'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
-                                                          'count' => $openingBraceStack[$lastOpeningBrace]['count'],
-                                                          'title' => '',
-                                                          'parts' => null,
-                                                          'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
-                               $openingBraceStack[$lastOpeningBrace--] = null;
-
-                               if ($matchingCount < $piece['count']) {
-                                       $piece['count'] -= $matchingCount;
-                                       $piece['startAt'] -= $matchingCount;
-                                       $piece['partStart'] = $piece['startAt'];
-                                       # do we still qualify for any callback with remaining count?
-                                       $currentCbList = $callbacks[$piece['brace']]['cb'];
-                                       while ( $piece['count'] ) {
-                                               if ( array_key_exists( $piece['count'], $currentCbList ) ) {
-                                                       $lastOpeningBrace++;
-                                                       $openingBraceStack[$lastOpeningBrace] = $piece;
-                                                       break;
-                                               }
-                                               --$piece['count'];
-                                       }
-                               }
-                       } elseif ( $found == 'pipe' ) {
-                               # lets set a title if it is a first separator, or next part otherwise
-                               if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
-                                       $openingBraceStack[$lastOpeningBrace]['title'] =
-                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
-                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-                                       $openingBraceStack[$lastOpeningBrace]['parts'] = array();
-                               } else {
-                                       $openingBraceStack[$lastOpeningBrace]['parts'][] =
-                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
-                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
-                               }
-                               $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i;
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
-
-       /**
-        * Replace magic variables, templates, and template arguments
-        * with the appropriate text. Templates are substituted recursively,
-        * taking care to avoid infinite loops.
-        *
-        * Note that the substitution depends on value of $mOutputType:
-        *  self::OT_WIKI: only {{subst:}} templates
-        *  self::OT_MSG: only magic variables
-        *  self::OT_HTML: all templates and magic variables
-        *
-        * @param string $tex The text to transform
-        * @param array $args Key-value pairs representing template parameters to substitute
-        * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
-        * @private
-        */
-       function replaceVariables( $text, $args = array(), $argsOnly = false ) {
-               # Prevent too big inclusions
-               if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
-                       return $text;
-               }
-
-               $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
-               wfProfileIn( $fname );
-
-               # This function is called recursively. To keep track of arguments we need a stack:
-               array_push( $this->mArgStack, $args );
-
-               $braceCallbacks = array();
-               if ( !$argsOnly ) {
-                       $braceCallbacks[2] = array( &$this, 'braceSubstitution' );
-               }
-               if ( $this->mOutputType != self::OT_MSG ) {
-                       $braceCallbacks[3] = array( &$this, 'argSubstitution' );
-               }
-               if ( $braceCallbacks ) {
-                       $callbacks = array(
-                               '{' => array(
-                                       'end' => '}',
-                                       'cb' => $braceCallbacks,
-                                       'min' => $argsOnly ? 3 : 2,
-                                       'max' => isset( $braceCallbacks[3] ) ? 3 : 2,
-                               ),
-                               '[' => array(
-                                       'end' => ']',
-                                       'cb' => array(2=>null),
-                                       'min' => 2,
-                                       'max' => 2,
-                               )
-                       );
-                       $text = $this->replace_callback ($text, $callbacks);
-
-                       array_pop( $this->mArgStack );
-               }
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * Replace magic variables
-        * @private
-        */
-       function variableSubstitution( $matches ) {
-               global $wgContLang;
-               $fname = 'Parser::variableSubstitution';
-               $varname = $wgContLang->lc($matches[1]);
-               wfProfileIn( $fname );
-               $skip = false;
-               if ( $this->mOutputType == self::OT_WIKI ) {
-                       # Do only magic variables prefixed by SUBST
-                       $mwSubst =& MagicWord::get( 'subst' );
-                       if (!$mwSubst->matchStartAndRemove( $varname ))
-                               $skip = true;
-                       # Note that if we don't substitute the variable below,
-                       # we don't remove the {{subst:}} magic word, in case
-                       # it is a template rather than a magic variable.
-               }
-               if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) {
-                       $id = $this->mVariables[$varname];
-                       # Now check if we did really match, case sensitive or not
-                       $mw =& MagicWord::get( $id );
-                       if ($mw->match($matches[1])) {
-                               $text = $this->getVariableValue( $id );
-                               if (MagicWord::getCacheTTL($id)>-1)
-                                       $this->mOutput->mContainsOldMagic = true;
-                       } else {
-                               $text = $matches[0];
-                       }
-               } else {
-                       $text = $matches[0];
-               }
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-
-       /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
-       static function createAssocArgs( $args ) {
-               $assocArgs = array();
-               $index = 1;
-               foreach( $args as $arg ) {
-                       $eqpos = strpos( $arg, '=' );
-                       if ( $eqpos === false ) {
-                               $assocArgs[$index++] = $arg;
-                       } else {
-                               $name = trim( substr( $arg, 0, $eqpos ) );
-                               $value = trim( substr( $arg, $eqpos+1 ) );
-                               if ( $value === false ) {
-                                       $value = '';
-                               }
-                               if ( $name !== false ) {
-                                       $assocArgs[$name] = $value;
-                               }
-                       }
-               }
-
-               return $assocArgs;
-       }
-
-       /**
-        * Return the text of a template, after recursively
-        * replacing any variables or templates within the template.
-        *
-        * @param array $piece The parts of the template
-        *  $piece['text']: matched text
-        *  $piece['title']: the title, i.e. the part before the |
-        *  $piece['parts']: the parameter array
-        * @return string the text of the template
-        * @private
-        */
-       function braceSubstitution( $piece ) {
-               global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
-               $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
-               wfProfileIn( $fname );
-               wfProfileIn( __METHOD__.'-setup' );
-
-               # Flags
-               $found = false;             # $text has been filled
-               $nowiki = false;            # wiki markup in $text should be escaped
-               $noparse = false;           # Unsafe HTML tags should not be stripped, etc.
-               $noargs = false;            # Don't replace triple-brace arguments in $text
-               $replaceHeadings = false;   # Make the edit section links go to the template not the article
-                $headingOffset = 0;         # Skip headings when number, to account for those that weren't transcluded.
-               $isHTML = false;            # $text is HTML, armour it against wikitext transformation
-               $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
-
-               # Title object, where $text came from
-               $title = NULL;
-
-               $linestart = '';
-
-
-               # $part1 is the bit before the first |, and must contain only title characters
-               # $args is a list of arguments, starting from index 0, not including $part1
-
-               $titleText = $part1 = $piece['title'];
-               # If the third subpattern matched anything, it will start with |
-
-               if (null == $piece['parts']) {
-                       $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title']));
-                       if ($replaceWith != $piece['text']) {
-                               $text = $replaceWith;
-                               $found = true;
-                               $noparse = true;
-                               $noargs = true;
-                       }
-               }
-
-               $args = (null == $piece['parts']) ? array() : $piece['parts'];
-               wfProfileOut( __METHOD__.'-setup' );
-
-               # SUBST
-               wfProfileIn( __METHOD__.'-modifiers' );
-               if ( !$found ) {
-                       $mwSubst =& MagicWord::get( 'subst' );
-                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
-                               # One of two possibilities is true:
-                               # 1) Found SUBST but not in the PST phase
-                               # 2) Didn't find SUBST and in the PST phase
-                               # In either case, return without further processing
-                               $text = $piece['text'];
-                               $found = true;
-                               $noparse = true;
-                               $noargs = true;
-                       }
-               }
-
-               # MSG, MSGNW and RAW
-               if ( !$found ) {
-                       # Check for MSGNW:
-                       $mwMsgnw =& MagicWord::get( 'msgnw' );
-                       if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
-                               $nowiki = true;
-                       } else {
-                               # Remove obsolete MSG:
-                               $mwMsg =& MagicWord::get( 'msg' );
-                               $mwMsg->matchStartAndRemove( $part1 );
-                       }
-
-                       # Check for RAW:
-                       $mwRaw =& MagicWord::get( 'raw' );
-                       if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
-                               $forceRawInterwiki = true;
-                       }
-               }
-               wfProfileOut( __METHOD__.'-modifiers' );
-
-               //save path level before recursing into functions & templates.
-               $lastPathLevel = $this->mTemplatePath;
-
-               # Parser functions
-               if ( !$found ) {
-                       wfProfileIn( __METHOD__ . '-pfunc' );
-
-                       $colonPos = strpos( $part1, ':' );
-                       if ( $colonPos !== false ) {
-                               # Case sensitive functions
-                               $function = substr( $part1, 0, $colonPos );
-                               if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
-                                       $function = $this->mFunctionSynonyms[1][$function];
-                               } else {
-                                       # Case insensitive functions
-                                       $function = strtolower( $function );
-                                       if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
-                                               $function = $this->mFunctionSynonyms[0][$function];
-                                       } else {
-                                               $function = false;
-                                       }
-                               }
-                               if ( $function ) {
-                                       $funcArgs = array_map( 'trim', $args );
-                                       $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs );
-                                       $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs );
-                                       $found = true;
-
-                                       // The text is usually already parsed, doesn't need triple-brace tags expanded, etc.
-                                       //$noargs = true;
-                                       //$noparse = true;
-
-                                       if ( is_array( $result ) ) {
-                                               if ( isset( $result[0] ) ) {
-                                                       $text = $linestart . $result[0];
-                                                       unset( $result[0] );
-                                               }
-
-                                               // Extract flags into the local scope
-                                               // This allows callers to set flags such as nowiki, noparse, found, etc.
-                                               extract( $result );
-                                       } else {
-                                               $text = $linestart . $result;
-                                       }
-                               }
-                       }
-                       wfProfileOut( __METHOD__ . '-pfunc' );
-               }
-
-               # Template table test
-
-               # Did we encounter this template already? If yes, it is in the cache
-               # and we need to check for loops.
-               if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) {
-                       $found = true;
-
-                       # Infinite loop test
-                       if ( isset( $this->mTemplatePath[$part1] ) ) {
-                               $noparse = true;
-                               $noargs = true;
-                               $found = true;
-                               $text = $linestart .
-                                       "[[$part1]]<!-- WARNING: template loop detected -->";
-                               wfDebug( __METHOD__.": template loop broken at '$part1'\n" );
-                       } else {
-                               # set $text to cached message.
-                               $text = $linestart . $this->mTemplates[$piece['title']];
-                               #treat title for cached page the same as others
-                               $ns = NS_TEMPLATE;
-                               $subpage = '';
-                               $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
-                               if ($subpage !== '') {
-                                 $ns = $this->mTitle->getNamespace();
-                               }
-                               $title = Title::newFromText( $part1, $ns );
-                               //used by include size checking
-                               $titleText = $title->getPrefixedText();
-                               //used by edit section links
-                               $replaceHeadings = true;
-
-                       }
-               }
-
-               # Load from database
-               if ( !$found ) {
-                       wfProfileIn( __METHOD__ . '-loadtpl' );
-                       $ns = NS_TEMPLATE;
-                       # declaring $subpage directly in the function call
-                       # does not work correctly with references and breaks
-                       # {{/subpage}}-style inclusions
-                       $subpage = '';
-                       $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
-                       if ($subpage !== '') {
-                               $ns = $this->mTitle->getNamespace();
-                       }
-                       $title = Title::newFromText( $part1, $ns );
-
-
-                       if ( !is_null( $title ) ) {
-                               $titleText = $title->getPrefixedText();
-                               # Check for language variants if the template is not found
-                               if($wgContLang->hasVariants() && $title->getArticleID() == 0){
-                                       $wgContLang->findVariantLink($part1, $title);
-                               }
-
-                               if ( !$title->isExternal() ) {
-                                       if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
-                                               $text = SpecialPage::capturePath( $title );
-                                               if ( is_string( $text ) ) {
-                                                       $found = true;
-                                                       $noparse = true;
-                                                       $noargs = true;
-                                                       $isHTML = true;
-                                                       $this->disableCache();
-                                               }
-                                       } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
-                                               $found = false; //access denied
-                                               wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
-                                       } else {
-                                               list($articleContent,$title) = $this->fetchTemplateAndtitle( $title );
-                                               if ( $articleContent !== false ) {
-                                                       $found = true;
-                                                       $text = $articleContent;
-                                                       $replaceHeadings = true;
-                                               }
-                                       }
-
-                                       # If the title is valid but undisplayable, make a link to it
-                                       if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
-                                               $text = "[[:$titleText]]";
-                                               $found = true;
-                                       }
-                               } elseif ( $title->isTrans() ) {
-                                       // Interwiki transclusion
-                                       if ( $this->ot['html'] && !$forceRawInterwiki ) {
-                                               $text = $this->interwikiTransclude( $title, 'render' );
-                                               $isHTML = true;
-                                               $noparse = true;
-                                       } else {
-                                               $text = $this->interwikiTransclude( $title, 'raw' );
-                                               $replaceHeadings = true;
-                                       }
-                                       $found = true;
-                               }
-
-                               # Template cache array insertion
-                               # Use the original $piece['title'] not the mangled $part1, so that
-                               # modifiers such as RAW: produce separate cache entries
-                               if( $found ) {
-                                       if( $isHTML ) {
-                                               // A special page; don't store it in the template cache.
-                                       } else {
-                                               $this->mTemplates[$piece['title']] = $text;
-                                       }
-                                       $text = $linestart . $text;
-                               }
-                       }
-                       wfProfileOut( __METHOD__ . '-loadtpl' );
-               }
-
-               if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) {
-                       # Error, oversize inclusion
-                       $text = $linestart .
-                               "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->";
-                       $noparse = true;
-                       $noargs = true;
-               }
-
-               # Recursive parsing, escaping and link table handling
-               # Only for HTML output
-               if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
-                       $text = wfEscapeWikiText( $text );
-               } elseif ( !$this->ot['msg'] && $found ) {
-                       if ( $noargs ) {
-                               $assocArgs = array();
-                       } else {
-                               # Clean up argument array
-                               $assocArgs = self::createAssocArgs($args);
-                               # Add a new element to the templace recursion path
-                               $this->mTemplatePath[$part1] = 1;
-                       }
-
-                       if ( !$noparse ) {
-                               # If there are any <onlyinclude> tags, only include them
-                               if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) {
-                                       $replacer = new OnlyIncludeReplacer;
-                                       StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>',
-                                               array( &$replacer, 'replace' ), $text );
-                                       $text = $replacer->output;
-                               }
-                               # Remove <noinclude> sections and <includeonly> tags
-                               $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text );
-                               $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) );
-
-                               if( $this->ot['html'] || $this->ot['pre'] ) {
-                                       # Strip <nowiki>, <pre>, etc.
-                                       $text = $this->strip( $text, $this->mStripState );
-                                       if ( $this->ot['html'] ) {
-                                               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
-                                       } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) {
-                                               $text = Sanitizer::removeHTMLcomments( $text );
-                                       }
-                               }
-                               $text = $this->replaceVariables( $text, $assocArgs );
-
-                               # If the template begins with a table or block-level
-                               # element, it should be treated as beginning a new line.
-                               if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
-                                       $text = "\n" . $text;
-                               }
-                       } elseif ( !$noargs ) {
-                               # $noparse and !$noargs
-                               # Just replace the arguments, not any double-brace items
-                               # This is used for rendered interwiki transclusion
-                               $text = $this->replaceVariables( $text, $assocArgs, true );
-                       }
-               }
-               # Prune lower levels off the recursion check path
-               $this->mTemplatePath = $lastPathLevel;
-
-               if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
-                       # Error, oversize inclusion
-                       $text = $linestart .
-                               "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->";
-                       $noparse = true;
-                       $noargs = true;
-               }
-
-               if ( !$found ) {
-                       wfProfileOut( $fname );
-                       return $piece['text'];
-               } else {
-                       wfProfileIn( __METHOD__ . '-placeholders' );
-                       if ( $isHTML ) {
-                               # Replace raw HTML by a placeholder
-                               # Add a blank line preceding, to prevent it from mucking up
-                               # immediately preceding headings
-                               $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState );
-                       } else {
-                               # replace ==section headers==
-                               # XXX this needs to go away once we have a better parser.
-                               if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) {
-                                       if( !is_null( $title ) )
-                                               $encodedname = base64_encode($title->getPrefixedDBkey());
-                                       else
-                                               $encodedname = base64_encode("");
-                                       $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1,
-                                               PREG_SPLIT_DELIM_CAPTURE);
-                                       $text = '';
-                                       $nsec = $headingOffset;
-
-                                       for( $i = 0; $i < count($m); $i += 2 ) {
-                                               $text .= $m[$i];
-                                               if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue;
-                                               $hl = $m[$i + 1];
-                                               if( strstr($hl, "<!--MWTEMPLATESECTION") ) {
-                                                       $text .= $hl;
-                                                       continue;
-                                               }
-                                               $m2 = array();
-                                               preg_match('/^(={1,6})(.*?)(={1,6}\s*?)$/m', $hl, $m2);
-                                               $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION="
-                                                       . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3];
-
-                                               $nsec++;
-                                       }
-                               }
-                       }
-                       wfProfileOut( __METHOD__ . '-placeholders' );
-               }
-
-               # Prune lower levels off the recursion check path
-               $this->mTemplatePath = $lastPathLevel;
-
-               if ( !$found ) {
-                       wfProfileOut( $fname );
-                       return $piece['text'];
-               } else {
-                       wfProfileOut( $fname );
-                       return $text;
-               }
-       }
-
-       /**
-        * Fetch the unparsed text of a template and register a reference to it.
-        */
-       function fetchTemplateAndTitle( $title ) {
-               $templateCb = $this->mOptions->getTemplateCallback();
-               $stuff = call_user_func( $templateCb, $title, $this );
-               $text = $stuff['text'];
-               $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
-               if ( isset( $stuff['deps'] ) ) {
-                       foreach ( $stuff['deps'] as $dep ) {
-                               $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
-                       }
-               }
-               return array($text,$finalTitle);
-       }
-
-       function fetchTemplate( $title ) {
-               $rv = $this->fetchTemplateAndtitle($title);
-               return $rv[0];
-       }
-
-       /**
-        * Static function to get a template
-        * Can be overridden via ParserOptions::setTemplateCallback().
-        *
-        * Returns an associative array:
-        *    text          The unparsed template text
-        *    finalTitle    (Optional) The title after following redirects
-        *    deps          (Optional) An array of associative array dependencies:
-        *                       title:    The dependency title, to be registered in templatelinks
-        *                       page_id:  The page_id of the title
-        *                       rev_id:   The revision ID loaded
-        */
-       static function statelessFetchTemplate( $title, $parser=false ) {
-               $text = $skip = false;
-               $finalTitle = $title;
-               $deps = array();
-
-               // Loop to fetch the article, with up to 1 redirect
-               for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
-                       # Give extensions a chance to select the revision instead
-                       $id = false; // Assume current
-                       wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
-
-                       if( $skip ) {
-                               $text = false;
-                               $deps[] = array(
-                                       'title' => $title,
-                                       'page_id' => $title->getArticleID(),
-                                       'rev_id' => null );
-                               break;
-                       }
-                       $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
-                       $rev_id = $rev ? $rev->getId() : 0;
-
-                       $deps[] = array(
-                               'title' => $title,
-                               'page_id' => $title->getArticleID(),
-                               'rev_id' => $rev_id );
-
-                       if( $rev ) {
-                               $text = $rev->getText();
-                       } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
-                               global $wgLang;
-                               $message = $wgLang->lcfirst( $title->getText() );
-                               $text = wfMsgForContentNoTrans( $message );
-                               if( wfEmptyMsg( $message, $text ) ) {
-                                       $text = false;
-                                       break;
-                               }
-                       } else {
-                               break;
-                       }
-                       if ( $text === false ) {
-                               break;
-                       }
-                       // Redirect?
-                       $finalTitle = $title;
-                       $title = Title::newFromRedirect( $text );
-               }
-               return array(
-                       'text' => $text,
-                       'finalTitle' => $finalTitle,
-                       'deps' => $deps );
-       }
-
-       /**
-        * Transclude an interwiki link.
-        */
-       function interwikiTransclude( $title, $action ) {
-               global $wgEnableScaryTranscluding;
-
-               if (!$wgEnableScaryTranscluding)
-                       return wfMsg('scarytranscludedisabled');
-
-               $url = $title->getFullUrl( "action=$action" );
-
-               if (strlen($url) > 255)
-                       return wfMsg('scarytranscludetoolong');
-               return $this->fetchScaryTemplateMaybeFromCache($url);
-       }
-
-       function fetchScaryTemplateMaybeFromCache($url) {
-               global $wgTranscludeCacheExpiry;
-               $dbr = wfGetDB(DB_SLAVE);
-               $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
-                               array('tc_url' => $url));
-               if ($obj) {
-                       $time = $obj->tc_time;
-                       $text = $obj->tc_contents;
-                       if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
-                               return $text;
-                       }
-               }
-
-               $text = Http::get($url);
-               if (!$text)
-                       return wfMsg('scarytranscludefailed', $url);
-
-               $dbw = wfGetDB(DB_MASTER);
-               $dbw->replace('transcache', array('tc_url'), array(
-                       'tc_url' => $url,
-                       'tc_time' => time(),
-                       'tc_contents' => $text));
-               return $text;
-       }
-
-
-       /**
-        * Triple brace replacement -- used for template arguments
-        * @private
-        */
-       function argSubstitution( $matches ) {
-               $arg = trim( $matches['title'] );
-               $text = $matches['text'];
-               $inputArgs = end( $this->mArgStack );
-
-               if ( array_key_exists( $arg, $inputArgs ) ) {
-                       $text = $inputArgs[$arg];
-               } else if (($this->mOutputType == self::OT_HTML || $this->mOutputType == self::OT_PREPROCESS ) &&
-               null != $matches['parts'] && count($matches['parts']) > 0) {
-                       $text = $matches['parts'][0];
-               }
-               if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
-                       $text = $matches['text'] .
-                               '<!-- WARNING: argument omitted, expansion size too large -->';
-               }
-
-               return $text;
-       }
-
-       /**
-        * Increment an include size counter
-        *
-        * @param string $type The type of expansion
-        * @param integer $size The size of the text
-        * @return boolean False if this inclusion would take it over the maximum, true otherwise
-        */
-       function incrementIncludeSize( $type, $size ) {
-               if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
-                       return false;
-               } else {
-                       $this->mIncludeSizes[$type] += $size;
-                       return true;
-               }
-       }
-
-       /**
-        * Detect __NOGALLERY__ magic word and set a placeholder
-        */
-       function stripNoGallery( &$text ) {
-               # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML,
-               # do not add TOC
-               $mw = MagicWord::get( 'nogallery' );
-               $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
-       }
-
-       /**
-        * Find the first __TOC__ magic word and set a <!--MWTOC-->
-        * placeholder that will then be replaced by the real TOC in
-        * ->formatHeadings, this works because at this points real
-        * comments will have already been discarded by the sanitizer.
-        *
-        * Any additional __TOC__ magic words left over will be discarded
-        * as there can only be one TOC on the page.
-        */
-       function stripToc( $text ) {
-               # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
-               # do not add TOC
-               $mw = MagicWord::get( 'notoc' );
-               if( $mw->matchAndRemove( $text ) ) {
-                       $this->mShowToc = false;
-               }
-
-               $mw = MagicWord::get( 'toc' );
-               if( $mw->match( $text ) ) {
-                       $this->mShowToc = true;
-                       $this->mForceTocPosition = true;
-
-                       // Set a placeholder. At the end we'll fill it in with the TOC.
-                       $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
-
-                       // Only keep the first one.
-                       $text = $mw->replace( '', $text );
-               }
-               return $text;
-       }
-
-       /**
-        * This function accomplishes several tasks:
-        * 1) Auto-number headings if that option is enabled
-        * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
-        * 3) Add a Table of contents on the top for users who have enabled the option
-        * 4) Auto-anchor headings
-        *
-        * It loops through all headlines, collects the necessary data, then splits up the
-        * string and re-inserts the newly formatted headlines.
-        *
-        * @param string $text
-        * @param boolean $isMain
-        * @private
-        */
-       function formatHeadings( $text, $isMain=true ) {
-               global $wgMaxTocLevel, $wgContLang;
-
-               $doNumberHeadings = $this->mOptions->getNumberHeadings();
-               if( !$this->mTitle->quickUserCan( 'edit' ) ) {
-                       $showEditLink = 0;
-               } else {
-                       $showEditLink = $this->mOptions->getEditSection();
-               }
-
-               # Inhibit editsection links if requested in the page
-               $esw =& MagicWord::get( 'noeditsection' );
-               if( $esw->matchAndRemove( $text ) ) {
-                       $showEditLink = 0;
-               }
-
-               # Get all headlines for numbering them and adding funky stuff like [edit]
-               # links - this is for later, but we need the number of headlines right now
-               $matches = array();
-               $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
-
-               # if there are fewer than 4 headlines in the article, do not show TOC
-               # unless it's been explicitly enabled.
-               $enoughToc = $this->mShowToc &&
-                       (($numMatches >= 4) || $this->mForceTocPosition);
-
-               # Allow user to stipulate that a page should have a "new section"
-               # link added via __NEWSECTIONLINK__
-               $mw =& MagicWord::get( 'newsectionlink' );
-               if( $mw->matchAndRemove( $text ) )
-                       $this->mOutput->setNewSection( true );
-
-               # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
-               # override above conditions and always show TOC above first header
-               $mw =& MagicWord::get( 'forcetoc' );
-               if ($mw->matchAndRemove( $text ) ) {
-                       $this->mShowToc = true;
-                       $enoughToc = true;
-               }
-
-               # We need this to perform operations on the HTML
-               $sk = $this->mOptions->getSkin();
-
-               # headline counter
-               $headlineCount = 0;
-               $sectionCount = 0; # headlineCount excluding template sections
-               $numVisible = 0;
-
-               # Ugh .. the TOC should have neat indentation levels which can be
-               # passed to the skin functions. These are determined here
-               $toc = '';
-               $full = '';
-               $head = array();
-               $sublevelCount = array();
-               $levelCount = array();
-               $toclevel = 0;
-               $level = 0;
-               $prevlevel = 0;
-               $toclevel = 0;
-               $prevtoclevel = 0;
-               $tocraw = array();
-
-               foreach( $matches[3] as $headline ) {
-                       $istemplate = 0;
-                       $templatetitle = '';
-                       $templatesection = 0;
-                       $numbering = '';
-                       $mat = array();
-                       if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) {
-                               $istemplate = 1;
-                               $templatetitle = base64_decode($mat[1]);
-                               $templatesection = 1 + (int)base64_decode($mat[2]);
-                               $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline);
-                       }
-
-                       if( $toclevel ) {
-                               $prevlevel = $level;
-                               $prevtoclevel = $toclevel;
-                       }
-                       $level = $matches[1][$headlineCount];
-
-                       if( $doNumberHeadings || $enoughToc ) {
-
-                               if ( $level > $prevlevel ) {
-                                       # Increase TOC level
-                                       $toclevel++;
-                                       $sublevelCount[$toclevel] = 0;
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               $prevtoclevel = $toclevel;
-                                               $toc .= $sk->tocIndent();
-                                               $numVisible++;
-                                       }
-                               }
-                               elseif ( $level < $prevlevel && $toclevel > 1 ) {
-                                       # Decrease TOC level, find level to jump to
-
-                                       if ( $toclevel == 2 && $level <= $levelCount[1] ) {
-                                               # Can only go down to level 1
-                                               $toclevel = 1;
-                                       } else {
-                                               for ($i = $toclevel; $i > 0; $i--) {
-                                                       if ( $levelCount[$i] == $level ) {
-                                                               # Found last matching level
-                                                               $toclevel = $i;
-                                                               break;
-                                                       }
-                                                       elseif ( $levelCount[$i] < $level ) {
-                                                               # Found first matching level below current level
-                                                               $toclevel = $i + 1;
-                                                               break;
-                                                       }
-                                               }
-                                       }
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               if($prevtoclevel < $wgMaxTocLevel) {
-                                                       # Unindent only if the previous toc level was shown :p
-                                                       $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
-                                               } else {
-                                                       $toc .= $sk->tocLineEnd();
-                                               }
-                                       }
-                               }
-                               else {
-                                       # No change in level, end TOC line
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               $toc .= $sk->tocLineEnd();
-                                       }
-                               }
-
-                               $levelCount[$toclevel] = $level;
-
-                               # count number of headlines for each level
-                               @$sublevelCount[$toclevel]++;
-                               $dot = 0;
-                               for( $i = 1; $i <= $toclevel; $i++ ) {
-                                       if( !empty( $sublevelCount[$i] ) ) {
-                                               if( $dot ) {
-                                                       $numbering .= '.';
-                                               }
-                                               $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
-                                               $dot = 1;
-                                       }
-                               }
-                       }
-
-                       # The canonized header is a version of the header text safe to use for links
-                       # Avoid insertion of weird stuff like <math> by expanding the relevant sections
-                       $canonized_headline = $this->mStripState->unstripBoth( $headline );
-
-                       # Remove link placeholders by the link text.
-                       #     <!--LINK number-->
-                       # turns into
-                       #     link text with suffix
-                       $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
-                                                           "\$this->mLinkHolders['texts'][\$1]",
-                                                           $canonized_headline );
-                       $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
-                                                           "\$this->mInterwikiLinkHolders['texts'][\$1]",
-                                                           $canonized_headline );
-
-                       # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
-                       $tocline = preg_replace(
-                               array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
-                               array( '',                          '<$1>'),
-                               $canonized_headline
-                       );
-                       $tocline = trim( $tocline );
-
-                       # For the anchor, strip out HTML-y stuff period
-                       $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline );
-                       $canonized_headline = trim( $canonized_headline );
-
-                       # Save headline for section edit hint before it's escaped
-                       $headline_hint = $canonized_headline;
-                       $canonized_headline = Sanitizer::escapeId( $canonized_headline );
-                       $refers[$headlineCount] = $canonized_headline;
-
-                       # count how many in assoc. array so we can track dupes in anchors
-                       isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1;
-                       $refcount[$headlineCount]=$refers[$canonized_headline];
-
-                       # Don't number the heading if it is the only one (looks silly)
-                       if( $doNumberHeadings && count( $matches[3] ) > 1) {
-                               # the two are different if the line contains a link
-                               $headline=$numbering . ' ' . $headline;
-                       }
-
-                       # Create the anchor for linking from the TOC to the section
-                       $anchor = $canonized_headline;
-                       if($refcount[$headlineCount] > 1 ) {
-                               $anchor .= '_' . $refcount[$headlineCount];
-                       }
-                       if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
-                               $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
-                               $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
-                       }
-                       # give headline the correct <h#> tag
-                       if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
-                               if( $istemplate )
-                                       $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection);
-                               else
-                                       $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint);
-                       } else {
-                               $editlink = '';
-                       }
-                       $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
-
-                       $headlineCount++;
-                       if( !$istemplate )
-                               $sectionCount++;
-               }
-
-               $this->mOutput->setSections( $tocraw );
-
-               # Never ever show TOC if no headers
-               if( $numVisible < 1 ) {
-                       $enoughToc = false;
-               }
-
-               if( $enoughToc ) {
-                       if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
-                               $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
-                       }
-                       $toc = $sk->tocList( $toc );
-               }
-
-               # split up and insert constructed headlines
-
-               $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
-               $i = 0;
-
-               foreach( $blocks as $block ) {
-                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
-                               # This is the [edit] link that appears for the top block of text when
-                               # section editing is enabled
-
-                               # Disabled because it broke block formatting
-                               # For example, a bullet point in the top line
-                               # $full .= $sk->editSectionLink(0);
-                       }
-                       $full .= $block;
-                       if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
-                               # Top anchor now in skin
-                               $full = $full.$toc;
-                       }
-
-                       if( !empty( $head[$i] ) ) {
-                               $full .= $head[$i];
-                       }
-                       $i++;
-               }
-               if( $this->mForceTocPosition ) {
-                       return str_replace( '<!--MWTOC-->', $toc, $full );
-               } else {
-                       return $full;
-               }
-       }
-
-       /**
-        * Transform wiki markup when saving a page by doing \r\n -> \n
-        * conversion, substitting signatures, {{subst:}} templates, etc.
-        *
-        * @param string $text the text to transform
-        * @param Title &$title the Title object for the current article
-        * @param User &$user the User object describing the current user
-        * @param ParserOptions $options parsing options
-        * @param bool $clearState whether to clear the parser state first
-        * @return string the altered wiki markup
-        * @public
-        */
-       function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
-               $this->mOptions = $options;
-               $this->mTitle =& $title;
-               $this->setOutputType( self::OT_WIKI );
-
-               if ( $clearState ) {
-                       $this->clearState();
-               }
-
-               $stripState = new StripState;
-               $pairs = array(
-                       "\r\n" => "\n",
-               );
-               $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
-               $text = $this->strip( $text, $stripState, true, array( 'gallery' ) );
-               $text = $this->pstPass2( $text, $stripState, $user );
-               $text = $stripState->unstripBoth( $text );
-               return $text;
-       }
-
-       /**
-        * Pre-save transform helper function
-        * @private
-        */
-       function pstPass2( $text, &$stripState, $user ) {
-               global $wgContLang, $wgLocaltimezone;
-
-               /* Note: This is the timestamp saved as hardcoded wikitext to
-                * the database, we use $wgContLang here in order to give
-                * everyone the same signature and use the default one rather
-                * than the one selected in each user's preferences.
-                */
-               if ( isset( $wgLocaltimezone ) ) {
-                       $oldtz = getenv( 'TZ' );
-                       putenv( 'TZ='.$wgLocaltimezone );
-               }
-               $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) .
-                 ' (' . date( 'T' ) . ')';
-               if ( isset( $wgLocaltimezone ) ) {
-                       putenv( 'TZ='.$oldtz );
-               }
-
-               # Variable replacement
-               # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
-               $text = $this->replaceVariables( $text );
-
-               # Strip out <nowiki> etc. added via replaceVariables
-               $text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
-
-               # Signatures
-               $sigText = $this->getUserSig( $user );
-               $text = strtr( $text, array(
-                       '~~~~~' => $d,
-                       '~~~~' => "$sigText $d",
-                       '~~~' => $sigText
-               ) );
-
-               # Context links: [[|name]] and [[name (context)|]]
-               #
-               global $wgLegalTitleChars;
-               $tc = "[$wgLegalTitleChars]";
-               $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
-
-               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/";            # [[ns:page (context)|]]
-               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/";  # [[ns:page (context), context|]]
-               $p2 = "/\[\[\\|($tc+)]]/";                                      # [[|page]]
-
-               # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
-               $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
-               $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
-
-               $t = $this->mTitle->getText();
-               $m = array();
-               if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
-                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
-               } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
-                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
-               } else {
-                       # if there's no context, don't bother duplicating the title
-                       $text = preg_replace( $p2, '[[\\1]]', $text );
-               }
-
-               # Trim trailing whitespace
-               $text = rtrim( $text );
-
-               return $text;
-       }
-
-       /**
-        * Fetch the user's signature text, if any, and normalize to
-        * validated, ready-to-insert wikitext.
-        *
-        * @param User $user
-        * @return string
-        * @private
-        */
-       function getUserSig( &$user ) {
-               global $wgMaxSigChars;
-
-               $username = $user->getName();
-               $nickname = $user->getOption( 'nickname' );
-               $nickname = $nickname === '' ? $username : $nickname;
-
-               if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
-                       $nickname = $username;
-                       wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
-               } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
-                       # Sig. might contain markup; validate this
-                       if( $this->validateSig( $nickname ) !== false ) {
-                               # Validated; clean up (if needed) and return it
-                               return $this->cleanSig( $nickname, true );
-                       } else {
-                               # Failed to validate; fall back to the default
-                               $nickname = $username;
-                               wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
-                       }
-               }
-
-               // Make sure nickname doesnt get a sig in a sig
-               $nickname = $this->cleanSigInSig( $nickname );
-
-               # If we're still here, make it a link to the user page
-               $userText = wfEscapeWikiText( $username );
-               $nickText = wfEscapeWikiText( $nickname );
-               if ( $user->isAnon() )  {
-                       return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
-               } else {
-                       return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
-               }
-       }
-
-       /**
-        * Check that the user's signature contains no bad XML
-        *
-        * @param string $text
-        * @return mixed An expanded string, or false if invalid.
-        */
-       function validateSig( $text ) {
-               return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
-       }
-
-       /**
-        * Clean up signature text
-        *
-        * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
-        * 2) Substitute all transclusions
-        *
-        * @param string $text
-        * @param $parsing Whether we're cleaning (preferences save) or parsing
-        * @return string Signature text
-        */
-       function cleanSig( $text, $parsing = false ) {
-               global $wgTitle;
-               $this->startExternalParse( $this->mTitle, new ParserOptions(), $parsing ? self::OT_WIKI : self::OT_MSG );
-
-               $substWord = MagicWord::get( 'subst' );
-               $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
-               $substText = '{{' . $substWord->getSynonym( 0 );
-
-               $text = preg_replace( $substRegex, $substText, $text );
-               $text = $this->cleanSigInSig( $text );
-               $text = $this->replaceVariables( $text );
-
-               $this->clearState();
-               return $text;
-       }
-
-       /**
-        * Strip ~~~, ~~~~ and ~~~~~ out of signatures
-        * @param string $text
-        * @return string Signature text with /~{3,5}/ removed
-        */
-       function cleanSigInSig( $text ) {
-               $text = preg_replace( '/~{3,5}/', '', $text );
-               return $text;
-       }
-
-       /**
-        * Set up some variables which are usually set up in parse()
-        * so that an external function can call some class members with confidence
-        * @public
-        */
-       function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
-               $this->mTitle =& $title;
-               $this->mOptions = $options;
-               $this->setOutputType( $outputType );
-               if ( $clearState ) {
-                       $this->clearState();
-               }
-       }
-
-       /**
-        * Transform a MediaWiki message by replacing magic variables.
-        *
-        * @param string $text the text to transform
-        * @param ParserOptions $options  options
-        * @return string the text with variables substituted
-        * @public
-        */
-       function transformMsg( $text, $options ) {
-               global $wgTitle;
-               static $executing = false;
-
-               $fname = "Parser::transformMsg";
-
-               # Guard against infinite recursion
-               if ( $executing ) {
-                       return $text;
-               }
-               $executing = true;
-
-               wfProfileIn($fname);
-
-               if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) {
-                       $this->mTitle = $wgTitle;
-               } else {
-                       $this->mTitle = Title::newFromText('msg');
-               }
-               $this->mOptions = $options;
-               $this->setOutputType( self::OT_MSG );
-               $this->clearState();
-               $text = $this->replaceVariables( $text );
-
-               $executing = false;
-               wfProfileOut($fname);
-               return $text;
-       }
-
-       /**
-        * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
-        * The callback should have the following form:
-        *    function myParserHook( $text, $params, &$parser ) { ... }
-        *
-        * Transform and return $text. Use $parser for any required context, e.g. use
-        * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
-        *
-        * @public
-        *
-        * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
-        * @param mixed $callback The callback function (and object) to use for the tag
-        *
-        * @return The old value of the mTagHooks array associated with the hook
-        */
-       function setHook( $tag, $callback ) {
-               $tag = strtolower( $tag );
-               $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
-               $this->mTagHooks[$tag] = $callback;
-
-               return $oldVal;
-       }
-
-       function setTransparentTagHook( $tag, $callback ) {
-               $tag = strtolower( $tag );
-               $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
-               $this->mTransparentTagHooks[$tag] = $callback;
-
-               return $oldVal;
-       }
-
-       /**
-        * Create a function, e.g. {{sum:1|2|3}}
-        * The callback function should have the form:
-        *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
-        *
-        * The callback may either return the text result of the function, or an array with the text
-        * in element 0, and a number of flags in the other elements. The names of the flags are
-        * specified in the keys. Valid flags are:
-        *   found                     The text returned is valid, stop processing the template. This
-        *                             is on by default.
-        *   nowiki                    Wiki markup in the return value should be escaped
-        *   noparse                   Unsafe HTML tags should not be stripped, etc.
-        *   noargs                    Don't replace triple-brace arguments in the return value
-        *   isHTML                    The returned text is HTML, armour it against wikitext transformation
-        *
-        * @public
-        *
-        * @param string $id The magic word ID
-        * @param mixed $callback The callback function (and object) to use
-        * @param integer $flags a combination of the following flags:
-        *                SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
-        *
-        * @return The old callback function for this name, if any
-        */
-       function setFunctionHook( $id, $callback, $flags = 0 ) {
-               $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null;
-               $this->mFunctionHooks[$id] = $callback;
-
-               # Add to function cache
-               $mw = MagicWord::get( $id );
-               if( !$mw )
-                       throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
-
-               $synonyms = $mw->getSynonyms();
-               $sensitive = intval( $mw->isCaseSensitive() );
-
-               foreach ( $synonyms as $syn ) {
-                       # Case
-                       if ( !$sensitive ) {
-                               $syn = strtolower( $syn );
-                       }
-                       # Add leading hash
-                       if ( !( $flags & SFH_NO_HASH ) ) {
-                               $syn = '#' . $syn;
-                       }
-                       # Remove trailing colon
-                       if ( substr( $syn, -1, 1 ) == ':' ) {
-                               $syn = substr( $syn, 0, -1 );
-                       }
-                       $this->mFunctionSynonyms[$sensitive][$syn] = $id;
-               }
-               return $oldVal;
-       }
-
-       /**
-        * Get all registered function hook identifiers
-        *
-        * @return array
-        */
-       function getFunctionHooks() {
-               return array_keys( $this->mFunctionHooks );
-       }
-
-       /**
-        * Replace <!--LINK--> link placeholders with actual links, in the buffer
-        * Placeholders created in Skin::makeLinkObj()
-        * Returns an array of links found, indexed by PDBK:
-        *  0 - broken
-        *  1 - normal link
-        *  2 - stub
-        * $options is a bit field, RLH_FOR_UPDATE to select for update
-        */
-       function replaceLinkHolders( &$text, $options = 0 ) {
-               global $wgUser;
-               global $wgContLang;
-
-               $fname = 'Parser::replaceLinkHolders';
-               wfProfileIn( $fname );
-
-               $pdbks = array();
-               $colours = array();
-               $sk = $this->mOptions->getSkin();
-               $linkCache = LinkCache::singleton();
-
-               if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
-                       wfProfileIn( $fname.'-check' );
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $page = $dbr->tableName( 'page' );
-                       $threshold = $wgUser->getOption('stubthreshold');
-
-                       # Sort by namespace
-                       asort( $this->mLinkHolders['namespaces'] );
-
-                       # Generate query
-                       $query = false;
-                       $current = null;
-                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                               # Make title object
-                               $title = $this->mLinkHolders['titles'][$key];
-
-                               # Skip invalid entries.
-                               # Result will be ugly, but prevents crash.
-                               if ( is_null( $title ) ) {
-                                       continue;
-                               }
-                               $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
-
-                               # Check if it's a static known link, e.g. interwiki
-                               if ( $title->isAlwaysKnown() ) {
-                                       $colours[$pdbk] = 1;
-                               } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-                                       $colours[$pdbk] = 1;
-                                       $this->mOutput->addLink( $title, $id );
-                               } elseif ( $linkCache->isBadLink( $pdbk ) ) {
-                                       $colours[$pdbk] = 0;
-                               } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
-                                       $colours[$pdbk] = 0;
-                               } else {
-                                       # Not in the link cache, add it to the query
-                                       if ( !isset( $current ) ) {
-                                               $current = $ns;
-                                               $query =  "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect";
-                                               $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
-                                       } elseif ( $current != $ns ) {
-                                               $current = $ns;
-                                               $query .= ")) OR (page_namespace=$ns AND page_title IN(";
-                                       } else {
-                                               $query .= ', ';
-                                       }
-
-                                       $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
-                               }
-                       }
-                       if ( $query ) {
-                               $query .= '))';
-                               if ( $options & RLH_FOR_UPDATE ) {
-                                       $query .= ' FOR UPDATE';
-                               }
-
-                               $res = $dbr->query( $query, $fname );
-
-                               # Fetch data and form into an associative array
-                               # non-existent = broken
-                               # 1 = known
-                               # 2 = stub
-                               while ( $s = $dbr->fetchObject($res) ) {
-                                       $title = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                       $pdbk = $title->getPrefixedDBkey();
-                                       $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
-                                       $this->mOutput->addLink( $title, $s->page_id );
-
-                                       $colours[$pdbk] = ( $threshold == 0 || (
-                                                               $s->page_len >= $threshold || # always true if $threshold <= 0
-                                                               $s->page_is_redirect ||
-                                                               !MWNamespace::isContent( $s->page_namespace ) )
-                                                           ? 1 : 2 );
-                               }
-                       }
-                       wfProfileOut( $fname.'-check' );
-
-                       # Do a second query for different language variants of links and categories
-                       if( $wgContLang->hasVariants() ) {
-                               $linkBatch = new LinkBatch();
-                               $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
-                               $categoryMap = array(); // maps $category_variant => $category (dbkeys)
-                               $varCategories = array(); // category replacements oldDBkey => newDBkey
-
-                               $categories = $this->mOutput->getCategoryLinks();
-
-                               // Add variants of links to link batch
-                               foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                                       $title = $this->mLinkHolders['titles'][$key];
-                                       if ( is_null( $title ) )
-                                               continue;
-
-                                       $pdbk = $title->getPrefixedDBkey();
-                                       $titleText = $title->getText();
-
-                                       // generate all variants of the link title text
-                                       $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
-
-                                       // if link was not found (in first query), add all variants to query
-                                       if ( !isset($colours[$pdbk]) ){
-                                               foreach($allTextVariants as $textVariant){
-                                                       if($textVariant != $titleText){
-                                                               $variantTitle = Title::makeTitle( $ns, $textVariant );
-                                                               if(is_null($variantTitle)) continue;
-                                                               $linkBatch->addObj( $variantTitle );
-                                                               $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
-                                                       }
-                                               }
-                                       }
-                               }
-
-                               // process categories, check if a category exists in some variant
-                               foreach( $categories as $category ){
-                                       $variants = $wgContLang->convertLinkToAllVariants($category);
-                                       foreach($variants as $variant){
-                                               if($variant != $category){
-                                                       $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
-                                                       if(is_null($variantTitle)) continue;
-                                                       $linkBatch->addObj( $variantTitle );
-                                                       $categoryMap[$variant] = $category;
-                                               }
-                                       }
-                               }
-
-
-                               if ( !$linkBatch->isEmpty() ){
-                                       // construct query
-                                       $titleClause = $linkBatch->constructSet('page', $dbr);
-
-                                       $variantQuery =  "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect";
-
-                                       $variantQuery .= " FROM $page WHERE $titleClause";
-                                       if ( $options & RLH_FOR_UPDATE ) {
-                                               $variantQuery .= ' FOR UPDATE';
-                                       }
-
-                                       $varRes = $dbr->query( $variantQuery, $fname );
-
-                                       // for each found variants, figure out link holders and replace
-                                       while ( $s = $dbr->fetchObject($varRes) ) {
-
-                                               $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                               $varPdbk = $variantTitle->getPrefixedDBkey();
-                                               $vardbk = $variantTitle->getDBkey();
-
-                                               $holderKeys = array();
-                                               if(isset($variantMap[$varPdbk])){
-                                                       $holderKeys = $variantMap[$varPdbk];
-                                                       $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
-                                                       $this->mOutput->addLink( $variantTitle, $s->page_id );
-                                               }
-
-                                               // loop over link holders
-                                               foreach($holderKeys as $key){
-                                                       $title = $this->mLinkHolders['titles'][$key];
-                                                       if ( is_null( $title ) ) continue;
-
-                                                       $pdbk = $title->getPrefixedDBkey();
-
-                                                       if(!isset($colours[$pdbk])){
-                                                               // found link in some of the variants, replace the link holder data
-                                                               $this->mLinkHolders['titles'][$key] = $variantTitle;
-                                                               $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
-
-                                                               // set pdbk and colour
-                                                               $pdbks[$key] = $varPdbk;
-                                                               if ( $threshold >  0 ) {
-                                                                       $size = $s->page_len;
-                                                                       if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
-                                                                               $colours[$varPdbk] = 1;
-                                                                       } else {
-                                                                               $colours[$varPdbk] = 2;
-                                                                       }
-                                                               }
-                                                               else {
-                                                                       $colours[$varPdbk] = 1;
-                                                               }
-                                                       }
-                                               }
-
-                                               // check if the object is a variant of a category
-                                               if(isset($categoryMap[$vardbk])){
-                                                       $oldkey = $categoryMap[$vardbk];
-                                                       if($oldkey != $vardbk)
-                                                               $varCategories[$oldkey]=$vardbk;
-                                               }
-                                       }
-
-                                       // rebuild the categories in original order (if there are replacements)
-                                       if(count($varCategories)>0){
-                                               $newCats = array();
-                                               $originalCats = $this->mOutput->getCategories();
-                                               foreach($originalCats as $cat => $sortkey){
-                                                       // make the replacement
-                                                       if( array_key_exists($cat,$varCategories) )
-                                                               $newCats[$varCategories[$cat]] = $sortkey;
-                                                       else $newCats[$cat] = $sortkey;
-                                               }
-                                               $this->mOutput->setCategoryLinks($newCats);
-                                       }
-                               }
-                       }
-
-                       # Construct search and replace arrays
-                       wfProfileIn( $fname.'-construct' );
-                       $replacePairs = array();
-                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-                               $pdbk = $pdbks[$key];
-                               $searchkey = "<!--LINK $key-->";
-                               $title = $this->mLinkHolders['titles'][$key];
-                               if ( empty( $colours[$pdbk] ) ) {
-                                       $linkCache->addBadLinkObj( $title );
-                                       $colours[$pdbk] = 0;
-                                       $this->mOutput->addLink( $title, 0 );
-                                       $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               } elseif ( $colours[$pdbk] == 1 ) {
-                                       $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title,
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               } elseif ( $colours[$pdbk] == 2 ) {
-                                       $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title,
-                                                                       $this->mLinkHolders['texts'][$key],
-                                                                       $this->mLinkHolders['queries'][$key] );
-                               }
-                       }
-                       $replacer = new HashtableReplacer( $replacePairs, 1 );
-                       wfProfileOut( $fname.'-construct' );
-
-                       # Do the thing
-                       wfProfileIn( $fname.'-replace' );
-                       $text = preg_replace_callback(
-                               '/(<!--LINK .*?-->)/',
-                               $replacer->cb(),
-                               $text);
-
-                       wfProfileOut( $fname.'-replace' );
-               }
-
-               # Now process interwiki link holders
-               # This is quite a bit simpler than internal links
-               if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
-                       wfProfileIn( $fname.'-interwiki' );
-                       # Make interwiki link HTML
-                       $replacePairs = array();
-                       foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
-                               $title = $this->mInterwikiLinkHolders['titles'][$key];
-                               $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
-                       }
-                       $replacer = new HashtableReplacer( $replacePairs, 1 );
-
-                       $text = preg_replace_callback(
-                               '/<!--IWLINK (.*?)-->/',
-                               $replacer->cb(),
-                               $text );
-                       wfProfileOut( $fname.'-interwiki' );
-               }
-
-               wfProfileOut( $fname );
-               return $colours;
-       }
-
-       /**
-        * Replace <!--LINK--> link placeholders with plain text of links
-        * (not HTML-formatted).
-        * @param string $text
-        * @return string
-        */
-       function replaceLinkHoldersText( $text ) {
-               $fname = 'Parser::replaceLinkHoldersText';
-               wfProfileIn( $fname );
-
-               $text = preg_replace_callback(
-                       '/<!--(LINK|IWLINK) (.*?)-->/',
-                       array( &$this, 'replaceLinkHoldersTextCallback' ),
-                       $text );
-
-               wfProfileOut( $fname );
-               return $text;
-       }
-
-       /**
-        * @param array $matches
-        * @return string
-        * @private
-        */
-       function replaceLinkHoldersTextCallback( $matches ) {
-               $type = $matches[1];
-               $key  = $matches[2];
-               if( $type == 'LINK' ) {
-                       if( isset( $this->mLinkHolders['texts'][$key] ) ) {
-                               return $this->mLinkHolders['texts'][$key];
-                       }
-               } elseif( $type == 'IWLINK' ) {
-                       if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
-                               return $this->mInterwikiLinkHolders['texts'][$key];
-                       }
-               }
-               return $matches[0];
-       }
-
-       /**
-        * Tag hook handler for 'pre'.
-        */
-       function renderPreTag( $text, $attribs ) {
-               // Backwards-compatibility hack
-               $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
-
-               $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
-               return wfOpenElement( 'pre', $attribs ) .
-                       Xml::escapeTagsOnly( $content ) .
-                       '</pre>';
-       }
-
-       /**
-        * Renders an image gallery from a text with one line per image.
-        * text labels may be given by using |-style alternative text. E.g.
-        *   Image:one.jpg|The number "1"
-        *   Image:tree.jpg|A tree
-        * given as text will return the HTML of a gallery with two images,
-        * labeled 'The number "1"' and
-        * 'A tree'.
-        */
-       function renderImageGallery( $text, $params ) {
-               $ig = new ImageGallery();
-               $ig->setContextTitle( $this->mTitle );
-               $ig->setShowBytes( false );
-               $ig->setShowFilename( false );
-               $ig->setParser( $this );
-               $ig->setHideBadImages();
-               $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
-               $ig->useSkin( $this->mOptions->getSkin() );
-               $ig->mRevisionId = $this->mRevisionId;
-
-               if( isset( $params['caption'] ) ) {
-                       $caption = $params['caption'];
-                       $caption = htmlspecialchars( $caption );
-                       $caption = $this->replaceInternalLinks( $caption );
-                       $ig->setCaptionHtml( $caption );
-               }
-               if( isset( $params['perrow'] ) ) {
-                       $ig->setPerRow( $params['perrow'] );
-               }
-               if( isset( $params['widths'] ) ) {
-                       $ig->setWidths( $params['widths'] );
-               }
-               if( isset( $params['heights'] ) ) {
-                       $ig->setHeights( $params['heights'] );
-               }
-
-               wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
-
-               $lines = explode( "\n", $text );
-               foreach ( $lines as $line ) {
-                       # match lines like these:
-                       # Image:someimage.jpg|This is some image
-                       $matches = array();
-                       preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
-                       # Skip empty lines
-                       if ( count( $matches ) == 0 ) {
-                               continue;
-                       }
-                       $tp = Title::newFromText( $matches[1] );
-                       $nt =& $tp;
-                       if( is_null( $nt ) ) {
-                               # Bogus title. Ignore these so we don't bomb out later.
-                               continue;
-                       }
-                       if ( isset( $matches[3] ) ) {
-                               $label = $matches[3];
-                       } else {
-                               $label = '';
-                       }
-
-                       $pout = $this->parse( $label,
-                               $this->mTitle,
-                               $this->mOptions,
-                               false, // Strip whitespace...?
-                               false  // Don't clear state!
-                       );
-                       $html = $pout->getText();
-
-                       $ig->add( $nt, $html );
-
-                       # Only add real images (bug #5586)
-                       if ( $nt->getNamespace() == NS_IMAGE ) {
-                               $this->mOutput->addImage( $nt->getDBkey() );
-                       }
-               }
-               return $ig->toHTML();
-       }
-
-       function getImageParams( $handler ) {
-               if ( $handler ) {
-                       $handlerClass = get_class( $handler );
-               } else {
-                       $handlerClass = '';
-               }
-               if ( !isset( $this->mImageParams[$handlerClass]  ) ) {
-                       // Initialise static lists
-                       static $internalParamNames = array(
-                               'horizAlign' => array( 'left', 'right', 'center', 'none' ),
-                               'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
-                                       'bottom', 'text-bottom' ),
-                               'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
-                                       'upright', 'border' ),
-                       );
-                       static $internalParamMap;
-                       if ( !$internalParamMap ) {
-                               $internalParamMap = array();
-                               foreach ( $internalParamNames as $type => $names ) {
-                                       foreach ( $names as $name ) {
-                                               $magicName = str_replace( '-', '_', "img_$name" );
-                                               $internalParamMap[$magicName] = array( $type, $name );
-                                       }
-                               }
-                       }
-
-                       // Add handler params
-                       $paramMap = $internalParamMap;
-                       if ( $handler ) {
-                               $handlerParamMap = $handler->getParamMap();
-                               foreach ( $handlerParamMap as $magic => $paramName ) {
-                                       $paramMap[$magic] = array( 'handler', $paramName );
-                               }
-                       }
-                       $this->mImageParams[$handlerClass] = $paramMap;
-                       $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
-               }
-               return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
-       }
-
-       /**
-        * Parse image options text and use it to make an image
-        */
-       function makeImage( $title, $options ) {
-               # @TODO: let the MediaHandler specify its transform parameters
-               #
-               # Check if the options text is of the form "options|alt text"
-               # Options are:
-               #  * thumbnail          make a thumbnail with enlarge-icon and caption, alignment depends on lang
-               #  * left               no resizing, just left align. label is used for alt= only
-               #  * right              same, but right aligned
-               #  * none               same, but not aligned
-               #  * ___px              scale to ___ pixels width, no aligning. e.g. use in taxobox
-               #  * center             center the image
-               #  * framed             Keep original image size, no magnify-button.
-               #  * frameless          like 'thumb' but without a frame. Keeps user preferences for width
-               #  * upright            reduce width for upright images, rounded to full __0 px
-               #  * border             draw a 1px border around the image
-               # vertical-align values (no % or length right now):
-               #  * baseline
-               #  * sub
-               #  * super
-               #  * top
-               #  * text-top
-               #  * middle
-               #  * bottom
-               #  * text-bottom
-
-               $parts = array_map( 'trim', explode( '|', $options) );
-               $sk = $this->mOptions->getSkin();
-
-               # Give extensions a chance to select the file revision for us
-               $skip = $time = false;
-               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) );
-
-               if ( $skip ) {
-                       return $sk->makeLinkObj( $title );
-               }
-
-               # Get parameter map
-               $file = wfFindFile( $title, $time );
-               $handler = $file ? $file->getHandler() : false;
-
-               list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
-
-               # Process the input parameters
-               $caption = '';
-               $params = array( 'frame' => array(), 'handler' => array(),
-                       'horizAlign' => array(), 'vertAlign' => array() );
-               foreach( $parts as $part ) {
-                       list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
-                       if ( isset( $paramMap[$magicName] ) ) {
-                               list( $type, $paramName ) = $paramMap[$magicName];
-                               $params[$type][$paramName] = $value;
-
-                               // Special case; width and height come in one variable together
-                               if( $type == 'handler' && $paramName == 'width' ) {
-                                       $m = array();
-                                       if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) {
-                                               $params[$type]['width'] = intval( $m[1] );
-                                               $params[$type]['height'] = intval( $m[2] );
-                                       } else {
-                                               $params[$type]['width'] = intval( $value );
-                                       }
-                               }
-                       } else {
-                               $caption = $part;
-                       }
-               }
-
-               # Process alignment parameters
-               if ( $params['horizAlign'] ) {
-                       $params['frame']['align'] = key( $params['horizAlign'] );
-               }
-               if ( $params['vertAlign'] ) {
-                       $params['frame']['valign'] = key( $params['vertAlign'] );
-               }
-
-               # Validate the handler parameters
-               if ( $handler ) {
-                       foreach ( $params['handler'] as $name => $value ) {
-                               if ( !$handler->validateParam( $name, $value ) ) {
-                                       unset( $params['handler'][$name] );
-                               }
-                       }
-               }
-
-               # Strip bad stuff out of the alt text
-               $alt = $this->replaceLinkHoldersText( $caption );
-
-               # make sure there are no placeholders in thumbnail attributes
-               # that are later expanded to html- so expand them now and
-               # remove the tags
-               $alt = $this->mStripState->unstripBoth( $alt );
-               $alt = Sanitizer::stripAllTags( $alt );
-
-               $params['frame']['alt'] = $alt;
-               $params['frame']['caption'] = $caption;
-
-               # Linker does the rest
-               $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] );
-
-               # Give the handler a chance to modify the parser object
-               if ( $handler ) {
-                       $handler->parserTransformHook( $this, $file );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Set a flag in the output object indicating that the content is dynamic and
-        * shouldn't be cached.
-        */
-       function disableCache() {
-               wfDebug( "Parser output marked as uncacheable.\n" );
-               $this->mOutput->mCacheTime = -1;
-       }
-
-       /**#@+
-        * Callback from the Sanitizer for expanding items found in HTML attribute
-        * values, so they can be safely tested and escaped.
-        * @param string $text
-        * @param array $args
-        * @return string
-        * @private
-        */
-       function attributeStripCallback( &$text, $args ) {
-               $text = $this->replaceVariables( $text, $args );
-               $text = $this->mStripState->unstripBoth( $text );
-               return $text;
-       }
-
-       /**#@-*/
-
-       /**#@+
-        * Accessor/mutator
-        */
-       function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
-       function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
-       function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
-       /**#@-*/
-
-       /**#@+
-        * Accessor
-        */
-       function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
-       /**#@-*/
-
-
-       /**
-        * Break wikitext input into sections, and either pull or replace
-        * some particular section's text.
-        *
-        * External callers should use the getSection and replaceSection methods.
-        *
-        * @param $text Page wikitext
-        * @param $section Numbered section. 0 pulls the text before the first
-        *                 heading; other numbers will pull the given section
-        *                 along with its lower-level subsections.
-        * @param $mode One of "get" or "replace"
-        * @param $newtext Replacement text for section data.
-        * @return string for "get", the extracted section text.
-        *                for "replace", the whole page with the section replaced.
-        */
-       private function extractSections( $text, $section, $mode, $newtext='' ) {
-               # I.... _hope_ this is right.
-               # Otherwise, sometimes we don't have things initialized properly.
-               $this->clearState();
-
-               # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
-               # comments to be stripped as well)
-               $stripState = new StripState;
-
-               $oldOutputType = $this->mOutputType;
-               $oldOptions = $this->mOptions;
-               $this->mOptions = new ParserOptions();
-               $this->setOutputType( self::OT_WIKI );
-
-               $striptext = $this->strip( $text, $stripState, true );
-
-               $this->setOutputType( $oldOutputType );
-               $this->mOptions = $oldOptions;
-
-               # now that we can be sure that no pseudo-sections are in the source,
-               # split it up by section
-               $uniq = preg_quote( $this->uniqPrefix(), '/' );
-               $comment = "(?:$uniq-!--.*?QINU\x07)";
-               $secs = preg_split(
-                       "/
-                       (
-                               ^
-                               (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
-                               (=+) # Should this be limited to 6?
-                               .+?  # Section title...
-                               \\2  # Ending = count must match start
-                               (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok
-                               $
-                       |
-                               <h([1-6])\b.*?>
-                               .*?
-                               <\/h\\3\s*>
-                       )
-                       /mix",
-                       $striptext, -1,
-                       PREG_SPLIT_DELIM_CAPTURE);
-
-               if( $mode == "get" ) {
-                       if( $section == 0 ) {
-                               // "Section 0" returns the content before any other section.
-                               $rv = $secs[0];
-                       } else {
-                               //track missing section, will replace if found.
-                               $rv = $newtext;
-                       }
-               } elseif( $mode == "replace" ) {
-                       if( $section == 0 ) {
-                               $rv = $newtext . "\n\n";
-                               $remainder = true;
-                       } else {
-                               $rv = $secs[0];
-                               $remainder = false;
-                       }
-               }
-               $count = 0;
-               $sectionLevel = 0;
-               for( $index = 1; $index < count( $secs ); ) {
-                       $headerLine = $secs[$index++];
-                       if( $secs[$index] ) {
-                               // A wiki header
-                               $headerLevel = strlen( $secs[$index++] );
-                       } else {
-                               // An HTML header
-                               $index++;
-                               $headerLevel = intval( $secs[$index++] );
-                       }
-                       $content = $secs[$index++];
-
-                       $count++;
-                       if( $mode == "get" ) {
-                               if( $count == $section ) {
-                                       $rv = $headerLine . $content;
-                                       $sectionLevel = $headerLevel;
-                               } elseif( $count > $section ) {
-                                       if( $sectionLevel && $headerLevel > $sectionLevel ) {
-                                               $rv .= $headerLine . $content;
-                                       } else {
-                                               // Broke out to a higher-level section
-                                               break;
-                                       }
-                               }
-                       } elseif( $mode == "replace" ) {
-                               if( $count < $section ) {
-                                       $rv .= $headerLine . $content;
-                               } elseif( $count == $section ) {
-                                       $rv .= $newtext . "\n\n";
-                                       $sectionLevel = $headerLevel;
-                               } elseif( $count > $section ) {
-                                       if( $headerLevel <= $sectionLevel ) {
-                                               // Passed the section's sub-parts.
-                                               $remainder = true;
-                                       }
-                                       if( $remainder ) {
-                                               $rv .= $headerLine . $content;
-                                       }
-                               }
-                       }
-               }
-               if (is_string($rv))
-                       # reinsert stripped tags
-                       $rv = trim( $stripState->unstripBoth( $rv ) );
-
-               return $rv;
-       }
-
-       /**
-        * This function returns the text of a section, specified by a number ($section).
-        * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
-        * the first section before any such heading (section 0).
-        *
-        * If a section contains subsections, these are also returned.
-        *
-        * @param $text String: text to look in
-        * @param $section Integer: section number
-        * @param $deftext: default to return if section is not found
-        * @return string text of the requested section
-        */
-       public function getSection( $text, $section, $deftext='' ) {
-               return $this->extractSections( $text, $section, "get", $deftext );
-       }
-
-       public function replaceSection( $oldtext, $section, $text ) {
-               return $this->extractSections( $oldtext, $section, "replace", $text );
-       }
-
-       /**
-        * Get the timestamp associated with the current revision, adjusted for
-        * the default server-local timestamp
-        */
-       function getRevisionTimestamp() {
-               if ( is_null( $this->mRevisionTimestamp ) ) {
-                       wfProfileIn( __METHOD__ );
-                       global $wgContLang;
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
-                                       array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
-
-                       // Normalize timestamp to internal MW format for timezone processing.
-                       // This has the added side-effect of replacing a null value with
-                       // the current time, which gives us more sensible behavior for
-                       // previews.
-                       $timestamp = wfTimestamp( TS_MW, $timestamp );
-
-                       // The cryptic '' timezone parameter tells to use the site-default
-                       // timezone offset instead of the user settings.
-                       //
-                       // Since this value will be saved into the parser cache, served
-                       // to other users, and potentially even used inside links and such,
-                       // it needs to be consistent for all visitors.
-                       $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
-
-                       wfProfileOut( __METHOD__ );
-               }
-               return $this->mRevisionTimestamp;
-       }
-
-       /**
-        * Mutator for $mDefaultSort
-        *
-        * @param $sort New value
-        */
-       public function setDefaultSort( $sort ) {
-               $this->mDefaultSort = $sort;
-       }
-
-       /**
-        * Accessor for $mDefaultSort
-        * Will use the title/prefixed title if none is set
-        *
-        * @return string
-        */
-       public function getDefaultSort() {
-               if( $this->mDefaultSort !== false ) {
-                       return $this->mDefaultSort;
-               } else {
-                       return $this->mTitle->getNamespace() == NS_CATEGORY
-                                       ? $this->mTitle->getText()
-                                       : $this->mTitle->getPrefixedText();
-               }
-       }
-
-       /**
-        * Try to guess the section anchor name based on a wikitext fragment
-        * presumably extracted from a heading, for example "Header" from
-        * "== Header ==".
-        */
-       public function guessSectionNameFromWikiText( $text ) {
-               # Strip out wikitext links(they break the anchor)
-               $text = $this->stripSectionName( $text );
-               $headline = Sanitizer::decodeCharReferences( $text );
-               # strip out HTML
-               $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
-               $headline = trim( $headline );
-               $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
-               $replacearray = array(
-                       '%3A' => ':',
-                       '%' => '.'
-               );
-               return str_replace(
-                       array_keys( $replacearray ),
-                       array_values( $replacearray ),
-                       $sectionanchor );
-       }
-
-       /**
-        * Strips a text string of wikitext for use in a section anchor
-        *
-        * Accepts a text string and then removes all wikitext from the
-        * string and leaves only the resultant text (i.e. the result of
-        * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
-        * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
-        * to create valid section anchors by mimicing the output of the
-        * parser when headings are parsed.
-        *
-        * @param $text string Text string to be stripped of wikitext
-        * for use in a Section anchor
-        * @return Filtered text string
-        */
-       public function stripSectionName( $text ) {
-               # Strip internal link markup
-               $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
-               $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
-
-               # Strip external link markup (FIXME: Not Tolerant to blank link text
-               # I.E. [http://www.mediawiki.org] will render as [1] or something depending
-               # on how many empty links there are on the page - need to figure that out.
-               $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
-
-               # Parse wikitext quotes (italics & bold)
-               $text = $this->doQuotes($text);
-
-               # Strip HTML tags
-               $text = StringUtils::delimiterReplace( '<', '>', '', $text );
-               return $text;
-       }
-
-       /**
-        * strip/replaceVariables/unstrip for preprocessor regression testing
-        */
-       function srvus( $text ) {
-               $text = $this->strip( $text, $this->mStripState );
-               $text = Sanitizer::removeHTMLtags( $text );
-               $text = $this->replaceVariables( $text );
-               $text = preg_replace( '/<!--MWTEMPLATESECTION.*?-->/', '', $text );
-               $text = $this->mStripState->unstripBoth( $text );
-               return $text;
-       }
-}
diff --git a/includes/Preprocessor.php b/includes/Preprocessor.php
deleted file mode 100644 (file)
index 322e197..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-
-/**
- * @ingroup Parser
- */
-interface Preprocessor {
-       /** Create a new preprocessor object based on an initialised Parser object */
-       function __construct( $parser );
-
-       /** Create a new top-level frame for expansion of a page */
-       function newFrame();
-
-       /** Preprocess text to a PPNode */
-       function preprocessToObj( $text, $flags = 0 );
-}
-
-/**
- * @ingroup Parser
- */
-interface PPFrame {
-       const NO_ARGS = 1;
-       const NO_TEMPLATES = 2;
-       const STRIP_COMMENTS = 4;
-       const NO_IGNORE = 8;
-       const RECOVER_COMMENTS = 16;
-
-       const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet
-
-       /**
-        * Create a child frame
-        */
-       function newChild( $args = false, $title = false );
-
-       /**
-        * Expand a document tree node
-        */
-       function expand( $root, $flags = 0 );
-
-       /**
-        * Implode with flags for expand()
-        */
-       function implodeWithFlags( $sep, $flags /*, ... */ );
-
-       /**
-        * Implode with no flags specified
-        */
-       function implode( $sep /*, ... */ );
-
-       /**
-        * Makes an object that, when expand()ed, will be the same as one obtained
-        * with implode()
-        */
-       function virtualImplode( $sep /*, ... */ );
-
-       /**
-        * Virtual implode with brackets
-        */
-       function virtualBracketedImplode( $start, $sep, $end /*, ... */ );
-
-       /**
-        * Returns true if there are no arguments in this frame
-        */
-       function isEmpty();
-
-       /**
-        * Get an argument to this frame by name
-        */
-       function getArgument( $name );
-
-       /**
-        * Returns true if the infinite loop check is OK, false if a loop is detected
-        */
-       function loopCheck( $title );
-
-       /**
-        * Return true if the frame is a template frame
-        */
-       function isTemplate();
-}
-
-/**
- * There are three types of nodes:
- *     * Tree nodes, which have a name and contain other nodes as children
- *     * Array nodes, which also contain other nodes but aren't considered part of a tree
- *     * Leaf nodes, which contain the actual data
- *
- * This interface provides access to the tree structure and to the contents of array nodes,
- * but it does not provide access to the internal structure of leaf nodes. Access to leaf
- * data is provided via two means:
- *     * PPFrame::expand(), which provides expanded text
- *     * The PPNode::split*() functions, which provide metadata about certain types of tree node
- * @ingroup Parser
- */
-interface PPNode {
-       /**
-        * Get an array-type node containing the children of this node.
-        * Returns false if this is not a tree node.
-        */
-       function getChildren();
-
-       /**
-        * Get the first child of a tree node. False if there isn't one.
-        */
-       function getFirstChild();
-
-       /**
-        * Get the next sibling of any node. False if there isn't one
-        */
-       function getNextSibling();
-
-       /**
-        * Get all children of this tree node which have a given name.
-        * Returns an array-type node, or false if this is not a tree node.
-        */
-       function getChildrenOfType( $type );
-
-
-       /**
-        * Returns the length of the array, or false if this is not an array-type node
-        */
-       function getLength();
-
-       /**
-        * Returns an item of an array-type node
-        */
-       function item( $i );
-
-       /**
-        * Get the name of this node. The following names are defined here:
-        *
-        *    h             A heading node.
-        *    template      A double-brace node.
-        *    tplarg        A triple-brace node.
-        *    title         The first argument to a template or tplarg node.
-        *    part          Subsequent arguments to a template or tplarg node.
-        *    #nodelist     An array-type node
-        *
-        * The subclass may define various other names for tree and leaf nodes.
-        */
-       function getName();
-
-       /**
-        * Split a <part> node into an associative array containing:
-        *    name          PPNode name
-        *    index         String index
-        *    value         PPNode value
-        */
-       function splitArg();
-
-       /**
-        * Split an <ext> node into an associative array containing name, attr, inner and close
-        * All values in the resulting array are PPNodes. Inner and close are optional.
-        */
-       function splitExt();
-
-       /**
-        * Split an <h> node
-        */
-       function splitHeading();
-}
diff --git a/includes/Preprocessor_DOM.php b/includes/Preprocessor_DOM.php
deleted file mode 100644 (file)
index 01776cd..0000000
+++ /dev/null
@@ -1,1379 +0,0 @@
-<?php
-
-/**
- * @ingroup Parser
- */
-class Preprocessor_DOM implements Preprocessor {
-       var $parser, $memoryLimit;
-
-       function __construct( $parser ) {
-               $this->parser = $parser;
-               $mem = ini_get( 'memory_limit' );
-               $this->memoryLimit = false;
-               if ( strval( $mem ) !== '' && $mem != -1 ) {
-                       if ( preg_match( '/^\d+$/', $mem ) ) {
-                               $this->memoryLimit = $mem;
-                       } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
-                               $this->memoryLimit = $m[1] * 1048576;
-                       }
-               }
-       }
-
-       function newFrame() {
-               return new PPFrame_DOM( $this );
-       }
-
-       function memCheck() {
-               if ( $this->memoryLimit === false ) {
-                       return;
-               }
-               $usage = memory_get_usage();
-               if ( $usage > $this->memoryLimit * 0.9 ) {
-                       $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
-                       throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
-               }
-               return $usage <= $this->memoryLimit * 0.8;
-       }
-
-       /**
-        * Preprocess some wikitext and return the document tree.
-        * This is the ghost of Parser::replace_variables().
-        *
-        * @param string $text The text to parse
-        * @param integer flags Bitwise combination of:
-        *          Parser::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
-        *                                     included. Default is to assume a direct page view.
-        *
-        * The generated DOM tree must depend only on the input text and the flags.
-        * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
-        *
-        * Any flag added to the $flags parameter here, or any other parameter liable to cause a
-        * change in the DOM tree for a given text, must be passed through the section identifier
-        * in the section edit link and thus back to extractSections().
-        *
-        * The output of this function is currently only cached in process memory, but a persistent
-        * cache may be implemented at a later date which takes further advantage of these strict
-        * dependency requirements.
-        *
-        * @private
-        */
-       function preprocessToObj( $text, $flags = 0 ) {
-               wfProfileIn( __METHOD__ );
-               wfProfileIn( __METHOD__.'-makexml' );
-
-               $rules = array(
-                       '{' => array(
-                               'end' => '}',
-                               'names' => array(
-                                       2 => 'template',
-                                       3 => 'tplarg',
-                               ),
-                               'min' => 2,
-                               'max' => 3,
-                       ),
-                       '[' => array(
-                               'end' => ']',
-                               'names' => array( 2 => null ),
-                               'min' => 2,
-                               'max' => 2,
-                       )
-               );
-
-               $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
-
-               $xmlishElements = $this->parser->getStripList();
-               $enableOnlyinclude = false;
-               if ( $forInclusion ) {
-                       $ignoredTags = array( 'includeonly', '/includeonly' );
-                       $ignoredElements = array( 'noinclude' );
-                       $xmlishElements[] = 'noinclude';
-                       if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
-                               $enableOnlyinclude = true;
-                       }
-               } else {
-                       $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
-                       $ignoredElements = array( 'includeonly' );
-                       $xmlishElements[] = 'includeonly';
-               }
-               $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
-
-               // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
-               $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
-
-               $stack = new PPDStack;
-
-               $searchBase = "[{<\n"; #}
-               $revText = strrev( $text ); // For fast reverse searches
-
-               $i = 0;                     # Input pointer, starts out pointing to a pseudo-newline before the start
-               $accum =& $stack->getAccum();   # Current accumulator
-               $accum = '<root>';
-               $findEquals = false;            # True to find equals signs in arguments
-               $findPipe = false;              # True to take notice of pipe characters
-               $headingIndex = 1;
-               $inHeading = false;        # True if $i is inside a possible heading
-               $noMoreGT = false;         # True if there are no more greater-than (>) signs right of $i
-               $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
-               $fakeLineStart = true;     # Do a line-start run without outputting an LF character
-
-               while ( true ) {
-                       //$this->memCheck();
-
-                       if ( $findOnlyinclude ) {
-                               // Ignore all input up to the next <onlyinclude>
-                               $startPos = strpos( $text, '<onlyinclude>', $i );
-                               if ( $startPos === false ) {
-                                       // Ignored section runs to the end
-                                       $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
-                                       break;
-                               }
-                               $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
-                               $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
-                               $i = $tagEndPos;
-                               $findOnlyinclude = false;
-                       }
-
-                       if ( $fakeLineStart ) {
-                               $found = 'line-start';
-                               $curChar = '';
-                       } else {
-                               # Find next opening brace, closing brace or pipe
-                               $search = $searchBase;
-                               if ( $stack->top === false ) {
-                                       $currentClosing = '';
-                               } else {
-                                       $currentClosing = $stack->top->close;
-                                       $search .= $currentClosing;
-                               }
-                               if ( $findPipe ) {
-                                       $search .= '|';
-                               }
-                               if ( $findEquals ) {
-                                       // First equals will be for the template
-                                       $search .= '=';
-                               }
-                               $rule = null;
-                               # Output literal section, advance input counter
-                               $literalLength = strcspn( $text, $search, $i );
-                               if ( $literalLength > 0 ) {
-                                       $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
-                                       $i += $literalLength;
-                               }
-                               if ( $i >= strlen( $text ) ) {
-                                       if ( $currentClosing == "\n" ) {
-                                               // Do a past-the-end run to finish off the heading
-                                               $curChar = '';
-                                               $found = 'line-end';
-                                       } else {
-                                               # All done
-                                               break;
-                                       }
-                               } else {
-                                       $curChar = $text[$i];
-                                       if ( $curChar == '|' ) {
-                                               $found = 'pipe';
-                                       } elseif ( $curChar == '=' ) {
-                                               $found = 'equals';
-                                       } elseif ( $curChar == '<' ) {
-                                               $found = 'angle';
-                                       } elseif ( $curChar == "\n" ) {
-                                               if ( $inHeading ) {
-                                                       $found = 'line-end';
-                                               } else {
-                                                       $found = 'line-start';
-                                               }
-                                       } elseif ( $curChar == $currentClosing ) {
-                                               $found = 'close';
-                                       } elseif ( isset( $rules[$curChar] ) ) {
-                                               $found = 'open';
-                                               $rule = $rules[$curChar];
-                                       } else {
-                                               # Some versions of PHP have a strcspn which stops on null characters
-                                               # Ignore and continue
-                                               ++$i;
-                                               continue;
-                                       }
-                               }
-                       }
-
-                       if ( $found == 'angle' ) {
-                               $matches = false;
-                               // Handle </onlyinclude>
-                               if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
-                                       $findOnlyinclude = true;
-                                       continue;
-                               }
-
-                               // Determine element name
-                               if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
-                                       // Element name missing or not listed
-                                       $accum .= '&lt;';
-                                       ++$i;
-                                       continue;
-                               }
-                               // Handle comments
-                               if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
-                                       // To avoid leaving blank lines, when a comment is both preceded
-                                       // and followed by a newline (ignoring spaces), trim leading and
-                                       // trailing spaces and one of the newlines.
-
-                                       // Find the end
-                                       $endPos = strpos( $text, '-->', $i + 4 );
-                                       if ( $endPos === false ) {
-                                               // Unclosed comment in input, runs to end
-                                               $inner = substr( $text, $i );
-                                               $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
-                                               $i = strlen( $text );
-                                       } else {
-                                               // Search backwards for leading whitespace
-                                               $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
-                                               // Search forwards for trailing whitespace
-                                               // $wsEnd will be the position of the last space
-                                               $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
-                                               // Eat the line if possible
-                                               // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
-                                               // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
-                                               // it's a possible beneficial b/c break.
-                                               if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
-                                                       && substr( $text, $wsEnd + 1, 1 ) == "\n" )
-                                               {
-                                                       $startPos = $wsStart;
-                                                       $endPos = $wsEnd + 1;
-                                                       // Remove leading whitespace from the end of the accumulator
-                                                       // Sanity check first though
-                                                       $wsLength = $i - $wsStart;
-                                                       if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) {
-                                                               $accum = substr( $accum, 0, -$wsLength );
-                                                       }
-                                                       // Do a line-start run next time to look for headings after the comment
-                                                       $fakeLineStart = true;
-                                               } else {
-                                                       // No line to eat, just take the comment itself
-                                                       $startPos = $i;
-                                                       $endPos += 2;
-                                               }
-
-                                               if ( $stack->top ) {
-                                                       $part = $stack->top->getCurrentPart();
-                                                       if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
-                                                               // Comments abutting, no change in visual end
-                                                               $part->commentEnd = $wsEnd;
-                                                       } else {
-                                                               $part->visualEnd = $wsStart;
-                                                               $part->commentEnd = $endPos;
-                                                       }
-                                               }
-                                               $i = $endPos + 1;
-                                               $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
-                                               $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
-                                       }
-                                       continue;
-                               }
-                               $name = $matches[1];
-                               $lowerName = strtolower( $name );
-                               $attrStart = $i + strlen( $name ) + 1;
-
-                               // Find end of tag
-                               $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
-                               if ( $tagEndPos === false ) {
-                                       // Infinite backtrack
-                                       // Disable tag search to prevent worst-case O(N^2) performance
-                                       $noMoreGT = true;
-                                       $accum .= '&lt;';
-                                       ++$i;
-                                       continue;
-                               }
-
-                               // Handle ignored tags
-                               if ( in_array( $lowerName, $ignoredTags ) ) {
-                                       $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . '</ignore>';
-                                       $i = $tagEndPos + 1;
-                                       continue;
-                               }
-
-                               $tagStartPos = $i;
-                               if ( $text[$tagEndPos-1] == '/' ) {
-                                       $attrEnd = $tagEndPos - 1;
-                                       $inner = null;
-                                       $i = $tagEndPos + 1;
-                                       $close = '';
-                               } else {
-                                       $attrEnd = $tagEndPos;
-                                       // Find closing tag
-                                       if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
-                                               $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
-                                               $i = $matches[0][1] + strlen( $matches[0][0] );
-                                               $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
-                                       } else {
-                                               // No end tag -- let it run out to the end of the text.
-                                               $inner = substr( $text, $tagEndPos + 1 );
-                                               $i = strlen( $text );
-                                               $close = '';
-                                       }
-                               }
-                               // <includeonly> and <noinclude> just become <ignore> tags
-                               if ( in_array( $lowerName, $ignoredElements ) ) {
-                                       $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
-                                               . '</ignore>';
-                                       continue;
-                               }
-
-                               $accum .= '<ext>';
-                               if ( $attrEnd <= $attrStart ) {
-                                       $attr = '';
-                               } else {
-                                       $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
-                               }
-                               $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
-                                       // Note that the attr element contains the whitespace between name and attribute,
-                                       // this is necessary for precise reconstruction during pre-save transform.
-                                       '<attr>' . htmlspecialchars( $attr ) . '</attr>';
-                               if ( $inner !== null ) {
-                                       $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
-                               }
-                               $accum .= $close . '</ext>';
-                       }
-
-                       elseif ( $found == 'line-start' ) {
-                               // Is this the start of a heading?
-                               // Line break belongs before the heading element in any case
-                               if ( $fakeLineStart ) {
-                                       $fakeLineStart = false;
-                               } else {
-                                       $accum .= $curChar;
-                                       $i++;
-                               }
-
-                               $count = strspn( $text, '=', $i, 6 );
-                               if ( $count == 1 && $findEquals ) {
-                                       // DWIM: This looks kind of like a name/value separator
-                                       // Let's let the equals handler have it and break the potential heading
-                                       // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
-                               } elseif ( $count > 0 ) {
-                                       $piece = array(
-                                               'open' => "\n",
-                                               'close' => "\n",
-                                               'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ),
-                                               'startPos' => $i,
-                                               'count' => $count );
-                                       $stack->push( $piece );
-                                       $accum =& $stack->getAccum();
-                                       extract( $stack->getFlags() );
-                                       $i += $count;
-                               }
-                       }
-
-                       elseif ( $found == 'line-end' ) {
-                               $piece = $stack->top;
-                               // A heading must be open, otherwise \n wouldn't have been in the search list
-                               assert( $piece->open == "\n" );
-                               $part = $piece->getCurrentPart();
-                               // Search back through the input to see if it has a proper close
-                               // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
-                               $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
-                               $searchStart = $i - $wsLength;
-                               if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
-                                       // Comment found at line end
-                                       // Search for equals signs before the comment
-                                       $searchStart = $part->visualEnd;
-                                       $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
-                               }
-                               $count = $piece->count;
-                               $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
-                               if ( $equalsLength > 0 ) {
-                                       if ( $i - $equalsLength == $piece->startPos ) {
-                                               // This is just a single string of equals signs on its own line
-                                               // Replicate the doHeadings behaviour /={count}(.+)={count}/
-                                               // First find out how many equals signs there really are (don't stop at 6)
-                                               $count = $equalsLength;
-                                               if ( $count < 3 ) {
-                                                       $count = 0;
-                                               } else {
-                                                       $count = min( 6, intval( ( $count - 1 ) / 2 ) );
-                                               }
-                                       } else {
-                                               $count = min( $equalsLength, $count );
-                                       }
-                                       if ( $count > 0 ) {
-                                               // Normal match, output <h>
-                                               $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
-                                               $headingIndex++;
-                                       } else {
-                                               // Single equals sign on its own line, count=0
-                                               $element = $accum;
-                                       }
-                               } else {
-                                       // No match, no <h>, just pass down the inner text
-                                       $element = $accum;
-                               }
-                               // Unwind the stack
-                               $stack->pop();
-                               $accum =& $stack->getAccum();
-                               extract( $stack->getFlags() );
-
-                               // Append the result to the enclosing accumulator
-                               $accum .= $element;
-                               // Note that we do NOT increment the input pointer.
-                               // This is because the closing linebreak could be the opening linebreak of
-                               // another heading. Infinite loops are avoided because the next iteration MUST
-                               // hit the heading open case above, which unconditionally increments the
-                               // input pointer.
-                       }
-
-                       elseif ( $found == 'open' ) {
-                               # count opening brace characters
-                               $count = strspn( $text, $curChar, $i );
-
-                               # we need to add to stack only if opening brace count is enough for one of the rules
-                               if ( $count >= $rule['min'] ) {
-                                       # Add it to the stack
-                                       $piece = array(
-                                               'open' => $curChar,
-                                               'close' => $rule['end'],
-                                               'count' => $count,
-                                               'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
-                                       );
-
-                                       $stack->push( $piece );
-                                       $accum =& $stack->getAccum();
-                                       extract( $stack->getFlags() );
-                               } else {
-                                       # Add literal brace(s)
-                                       $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
-                               }
-                               $i += $count;
-                       }
-
-                       elseif ( $found == 'close' ) {
-                               $piece = $stack->top;
-                               # lets check if there are enough characters for closing brace
-                               $maxCount = $piece->count;
-                               $count = strspn( $text, $curChar, $i, $maxCount );
-
-                               # check for maximum matching characters (if there are 5 closing
-                               # characters, we will probably need only 3 - depending on the rules)
-                               $matchingCount = 0;
-                               $rule = $rules[$piece->open];
-                               if ( $count > $rule['max'] ) {
-                                       # The specified maximum exists in the callback array, unless the caller
-                                       # has made an error
-                                       $matchingCount = $rule['max'];
-                               } else {
-                                       # Count is less than the maximum
-                                       # Skip any gaps in the callback array to find the true largest match
-                                       # Need to use array_key_exists not isset because the callback can be null
-                                       $matchingCount = $count;
-                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
-                                               --$matchingCount;
-                                       }
-                               }
-
-                               if ($matchingCount <= 0) {
-                                       # No matching element found in callback array
-                                       # Output a literal closing brace and continue
-                                       $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
-                                       $i += $count;
-                                       continue;
-                               }
-                               $name = $rule['names'][$matchingCount];
-                               if ( $name === null ) {
-                                       // No element, just literal text
-                                       $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount );
-                               } else {
-                                       # Create XML element
-                                       # Note: $parts is already XML, does not need to be encoded further
-                                       $parts = $piece->parts;
-                                       $title = $parts[0]->out;
-                                       unset( $parts[0] );
-
-                                       # The invocation is at the start of the line if lineStart is set in
-                                       # the stack, and all opening brackets are used up.
-                                       if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
-                                               $attr = ' lineStart="1"';
-                                       } else {
-                                               $attr = '';
-                                       }
-
-                                       $element = "<$name$attr>";
-                                       $element .= "<title>$title</title>";
-                                       $argIndex = 1;
-                                       foreach ( $parts as $partIndex => $part ) {
-                                               if ( isset( $part->eqpos ) ) {
-                                                       $argName = substr( $part->out, 0, $part->eqpos );
-                                                       $argValue = substr( $part->out, $part->eqpos + 1 );
-                                                       $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
-                                               } else {
-                                                       $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
-                                                       $argIndex++;
-                                               }
-                                       }
-                                       $element .= "</$name>";
-                               }
-
-                               # Advance input pointer
-                               $i += $matchingCount;
-
-                               # Unwind the stack
-                               $stack->pop();
-                               $accum =& $stack->getAccum();
-
-                               # Re-add the old stack element if it still has unmatched opening characters remaining
-                               if ($matchingCount < $piece->count) {
-                                       $piece->parts = array( new PPDPart );
-                                       $piece->count -= $matchingCount;
-                                       # do we still qualify for any callback with remaining count?
-                                       $names = $rules[$piece->open]['names'];
-                                       $skippedBraces = 0;
-                                       $enclosingAccum =& $accum;
-                                       while ( $piece->count ) {
-                                               if ( array_key_exists( $piece->count, $names ) ) {
-                                                       $stack->push( $piece );
-                                                       $accum =& $stack->getAccum();
-                                                       break;
-                                               }
-                                               --$piece->count;
-                                               $skippedBraces ++;
-                                       }
-                                       $enclosingAccum .= str_repeat( $piece->open, $skippedBraces );
-                               }
-
-                               extract( $stack->getFlags() );
-
-                               # Add XML element to the enclosing accumulator
-                               $accum .= $element;
-                       }
-
-                       elseif ( $found == 'pipe' ) {
-                               $findEquals = true; // shortcut for getFlags()
-                               $stack->addPart();
-                               $accum =& $stack->getAccum();
-                               ++$i;
-                       }
-
-                       elseif ( $found == 'equals' ) {
-                               $findEquals = false; // shortcut for getFlags()
-                               $stack->getCurrentPart()->eqpos = strlen( $accum );
-                               $accum .= '=';
-                               ++$i;
-                       }
-               }
-
-               # Output any remaining unclosed brackets
-               foreach ( $stack->stack as $piece ) {
-                       $stack->rootAccum .= $piece->breakSyntax();
-               }
-               $stack->rootAccum .= '</root>';
-               $xml = $stack->rootAccum;
-
-               wfProfileOut( __METHOD__.'-makexml' );
-               wfProfileIn( __METHOD__.'-loadXML' );
-               $dom = new DOMDocument;
-               wfSuppressWarnings();
-               $result = $dom->loadXML( $xml );
-               wfRestoreWarnings();
-               if ( !$result ) {
-                       // Try running the XML through UtfNormal to get rid of invalid characters
-                       $xml = UtfNormal::cleanUp( $xml );
-                       $result = $dom->loadXML( $xml );
-                       if ( !$result ) {
-                               throw new MWException( __METHOD__.' generated invalid XML' );
-                       }
-               }
-               $obj = new PPNode_DOM( $dom->documentElement );
-               wfProfileOut( __METHOD__.'-loadXML' );
-               wfProfileOut( __METHOD__ );
-               return $obj;
-       }
-}
-
-/**
- * Stack class to help Preprocessor::preprocessToObj()
- * @ingroup Parser
- */
-class PPDStack {
-       var $stack, $rootAccum, $top;
-       var $out;
-       var $elementClass = 'PPDStackElement';
-
-       static $false = false;
-
-       function __construct() {
-               $this->stack = array();
-               $this->top = false;
-               $this->rootAccum = '';
-               $this->accum =& $this->rootAccum;
-       }
-
-       function count() {
-               return count( $this->stack );
-       }
-
-       function &getAccum() {
-               return $this->accum;
-       }
-
-       function getCurrentPart() {
-               if ( $this->top === false ) {
-                       return false;
-               } else {
-                       return $this->top->getCurrentPart();
-               }
-       }
-
-       function push( $data ) {
-               if ( $data instanceof $this->elementClass ) {
-                       $this->stack[] = $data;
-               } else {
-                       $class = $this->elementClass;
-                       $this->stack[] = new $class( $data );
-               }
-               $this->top = $this->stack[ count( $this->stack ) - 1 ];
-               $this->accum =& $this->top->getAccum();
-       }
-
-       function pop() {
-               if ( !count( $this->stack ) ) {
-                       throw new MWException( __METHOD__.': no elements remaining' );
-               }
-               $temp = array_pop( $this->stack );
-
-               if ( count( $this->stack ) ) {
-                       $this->top = $this->stack[ count( $this->stack ) - 1 ];
-                       $this->accum =& $this->top->getAccum();
-               } else {
-                       $this->top = self::$false;
-                       $this->accum =& $this->rootAccum;
-               }
-               return $temp;
-       }
-
-       function addPart( $s = '' ) {
-               $this->top->addPart( $s );
-               $this->accum =& $this->top->getAccum();
-       }
-
-       function getFlags() {
-               if ( !count( $this->stack ) ) {
-                       return array(
-                               'findEquals' => false,
-                               'findPipe' => false,
-                               'inHeading' => false,
-                       );
-               } else {
-                       return $this->top->getFlags();
-               }
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDStackElement {
-       var $open,                      // Opening character (\n for heading)
-               $close,             // Matching closing character
-               $count,             // Number of opening characters found (number of "=" for heading)
-               $parts,             // Array of PPDPart objects describing pipe-separated parts.
-               $lineStart;         // True if the open char appeared at the start of the input line. Not set for headings.
-
-       var $partClass = 'PPDPart';
-
-       function __construct( $data = array() ) {
-               $class = $this->partClass;
-               $this->parts = array( new $class );
-
-               foreach ( $data as $name => $value ) {
-                       $this->$name = $value;
-               }
-       }
-
-       function &getAccum() {
-               return $this->parts[count($this->parts) - 1]->out;
-       }
-
-       function addPart( $s = '' ) {
-               $class = $this->partClass;
-               $this->parts[] = new $class( $s );
-       }
-
-       function getCurrentPart() {
-               return $this->parts[count($this->parts) - 1];
-       }
-
-       function getFlags() {
-               $partCount = count( $this->parts );
-               $findPipe = $this->open != "\n" && $this->open != '[';
-               return array(
-                       'findPipe' => $findPipe,
-                       'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
-                       'inHeading' => $this->open == "\n",
-               );
-       }
-
-       /**
-        * Get the output string that would result if the close is not found.
-        */
-       function breakSyntax( $openingCount = false ) {
-               if ( $this->open == "\n" ) {
-                       $s = $this->parts[0]->out;
-               } else {
-                       if ( $openingCount === false ) {
-                               $openingCount = $this->count;
-                       }
-                       $s = str_repeat( $this->open, $openingCount );
-                       $first = true;
-                       foreach ( $this->parts as $part ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $s .= '|';
-                               }
-                               $s .= $part->out;
-                       }
-               }
-               return $s;
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDPart {
-       var $out; // Output accumulator string
-
-       // Optional member variables:
-       //   eqpos        Position of equals sign in output accumulator
-       //   commentEnd   Past-the-end input pointer for the last comment encountered
-       //   visualEnd    Past-the-end input pointer for the end of the accumulator minus comments
-
-       function __construct( $out = '' ) {
-               $this->out = $out;
-       }
-}
-
-/**
- * An expansion frame, used as a context to expand the result of preprocessToObj()
- * @ingroup Parser
- */
-class PPFrame_DOM implements PPFrame {
-       var $preprocessor, $parser, $title;
-       var $titleCache;
-
-       /**
-        * Hashtable listing templates which are disallowed for expansion in this frame,
-        * having been encountered previously in parent frames.
-        */
-       var $loopCheckHash;
-
-       /**
-        * Recursion depth of this frame, top = 0
-        */
-       var $depth;
-
-
-       /**
-        * Construct a new preprocessor frame.
-        * @param Preprocessor $preprocessor The parent preprocessor
-        */
-       function __construct( $preprocessor ) {
-               $this->preprocessor = $preprocessor;
-               $this->parser = $preprocessor->parser;
-               $this->title = $this->parser->mTitle;
-               $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
-               $this->loopCheckHash = array();
-               $this->depth = 0;
-       }
-
-       /**
-        * Create a new child frame
-        * $args is optionally a multi-root PPNode or array containing the template arguments
-        */
-       function newChild( $args = false, $title = false ) {
-               $namedArgs = array();
-               $numberedArgs = array();
-               if ( $title === false ) {
-                       $title = $this->title;
-               }
-               if ( $args !== false ) {
-                       $xpath = false;
-                       if ( $args instanceof PPNode ) {
-                               $args = $args->node;
-                       }
-                       foreach ( $args as $arg ) {
-                               if ( !$xpath ) {
-                                       $xpath = new DOMXPath( $arg->ownerDocument );
-                               }
-
-                               $nameNodes = $xpath->query( 'name', $arg );
-                               $value = $xpath->query( 'value', $arg );
-                               if ( $nameNodes->item( 0 )->hasAttributes() ) {
-                                       // Numbered parameter
-                                       $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
-                                       $numberedArgs[$index] = $value->item( 0 );
-                                       unset( $namedArgs[$index] );
-                               } else {
-                                       // Named parameter
-                                       $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
-                                       $namedArgs[$name] = $value->item( 0 );
-                                       unset( $numberedArgs[$name] );
-                               }
-                       }
-               }
-               return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
-       }
-
-       function expand( $root, $flags = 0 ) {
-               static $depth = 0;
-               if ( is_string( $root ) ) {
-                       return $root;
-               }
-
-               if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
-               {
-                       return '<span class="error">Node-count limit exceeded</span>';
-               }
-
-               if ( $depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
-                       return '<span class="error">Expansion depth limit exceeded</span>';
-               }
-               ++$depth;
-
-               if ( $root instanceof PPNode_DOM ) {
-                       $root = $root->node;
-               }
-               if ( $root instanceof DOMDocument ) {
-                       $root = $root->documentElement;
-               }
-
-               $outStack = array( '', '' );
-               $iteratorStack = array( false, $root );
-               $indexStack = array( 0, 0 );
-
-               while ( count( $iteratorStack ) > 1 ) {
-                       $level = count( $outStack ) - 1;
-                       $iteratorNode =& $iteratorStack[ $level ];
-                       $out =& $outStack[$level];
-                       $index =& $indexStack[$level];
-
-                       if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node;
-
-                       if ( is_array( $iteratorNode ) ) {
-                               if ( $index >= count( $iteratorNode ) ) {
-                                       // All done with this iterator
-                                       $iteratorStack[$level] = false;
-                                       $contextNode = false;
-                               } else {
-                                       $contextNode = $iteratorNode[$index];
-                                       $index++;
-                               }
-                       } elseif ( $iteratorNode instanceof DOMNodeList ) {
-                               if ( $index >= $iteratorNode->length ) {
-                                       // All done with this iterator
-                                       $iteratorStack[$level] = false;
-                                       $contextNode = false;
-                               } else {
-                                       $contextNode = $iteratorNode->item( $index );
-                                       $index++;
-                               }
-                       } else {
-                               // Copy to $contextNode and then delete from iterator stack,
-                               // because this is not an iterator but we do have to execute it once
-                               $contextNode = $iteratorStack[$level];
-                               $iteratorStack[$level] = false;
-                       }
-
-                       if ( $contextNode instanceof PPNode_DOM ) $contextNode = $contextNode->node;
-
-                       $newIterator = false;
-
-                       if ( $contextNode === false ) {
-                               // nothing to do
-                       } elseif ( is_string( $contextNode ) ) {
-                               $out .= $contextNode;
-                       } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
-                               $newIterator = $contextNode;
-                       } elseif ( $contextNode instanceof DOMNode ) {
-                               if ( $contextNode->nodeType == XML_TEXT_NODE ) {
-                                       $out .= $contextNode->nodeValue;
-                               } elseif ( $contextNode->nodeName == 'template' ) {
-                                       # Double-brace expansion
-                                       $xpath = new DOMXPath( $contextNode->ownerDocument );
-                                       $titles = $xpath->query( 'title', $contextNode );
-                                       $title = $titles->item( 0 );
-                                       $parts = $xpath->query( 'part', $contextNode );
-                                       if ( $flags & self::NO_TEMPLATES ) {
-                                               $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
-                                       } else {
-                                               $lineStart = $contextNode->getAttribute( 'lineStart' );
-                                               $params = array(
-                                                       'title' => new PPNode_DOM( $title ),
-                                                       'parts' => new PPNode_DOM( $parts ),
-                                                       'lineStart' => $lineStart );
-                                               $ret = $this->parser->braceSubstitution( $params, $this );
-                                               if ( isset( $ret['object'] ) ) {
-                                                       $newIterator = $ret['object'];
-                                               } else {
-                                                       $out .= $ret['text'];
-                                               }
-                                       }
-                               } elseif ( $contextNode->nodeName == 'tplarg' ) {
-                                       # Triple-brace expansion
-                                       $xpath = new DOMXPath( $contextNode->ownerDocument );
-                                       $titles = $xpath->query( 'title', $contextNode );
-                                       $title = $titles->item( 0 );
-                                       $parts = $xpath->query( 'part', $contextNode );
-                                       if ( $flags & self::NO_ARGS ) {
-                                               $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
-                                       } else {
-                                               $params = array(
-                                                       'title' => new PPNode_DOM( $title ),
-                                                       'parts' => new PPNode_DOM( $parts ) );
-                                               $ret = $this->parser->argSubstitution( $params, $this );
-                                               if ( isset( $ret['object'] ) ) {
-                                                       $newIterator = $ret['object'];
-                                               } else {
-                                                       $out .= $ret['text'];
-                                               }
-                                       }
-                               } elseif ( $contextNode->nodeName == 'comment' ) {
-                                       # HTML-style comment
-                                       # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
-                                       if ( $this->parser->ot['html']
-                                               || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
-                                               || ( $flags & self::STRIP_COMMENTS ) )
-                                       {
-                                               $out .= '';
-                                       }
-                                       # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
-                                       # Not in RECOVER_COMMENTS mode (extractSections) though
-                                       elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
-                                               $out .= $this->parser->insertStripItem( $contextNode->textContent );
-                                       }
-                                       # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
-                                       else {
-                                               $out .= $contextNode->textContent;
-                                       }
-                               } elseif ( $contextNode->nodeName == 'ignore' ) {
-                                       # Output suppression used by <includeonly> etc.
-                                       # OT_WIKI will only respect <ignore> in substed templates.
-                                       # The other output types respect it unless NO_IGNORE is set.
-                                       # extractSections() sets NO_IGNORE and so never respects it.
-                                       if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
-                                               $out .= $contextNode->textContent;
-                                       } else {
-                                               $out .= '';
-                                       }
-                               } elseif ( $contextNode->nodeName == 'ext' ) {
-                                       # Extension tag
-                                       $xpath = new DOMXPath( $contextNode->ownerDocument );
-                                       $names = $xpath->query( 'name', $contextNode );
-                                       $attrs = $xpath->query( 'attr', $contextNode );
-                                       $inners = $xpath->query( 'inner', $contextNode );
-                                       $closes = $xpath->query( 'close', $contextNode );
-                                       $params = array(
-                                               'name' => new PPNode_DOM( $names->item( 0 ) ),
-                                               'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
-                                               'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
-                                               'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
-                                       );
-                                       $out .= $this->parser->extensionSubstitution( $params, $this );
-                               } elseif ( $contextNode->nodeName == 'h' ) {
-                                       # Heading
-                                       $s = $this->expand( $contextNode->childNodes, $flags );
-
-                    # Insert a heading marker only for <h> children of <root>
-                    # This is to stop extractSections from going over multiple tree levels
-                    if ( $contextNode->parentNode->nodeName == 'root'
-                      && $this->parser->ot['html'] )
-                    {
-                                               # Insert heading index marker
-                                               $headingIndex = $contextNode->getAttribute( 'i' );
-                                               $titleText = $this->title->getPrefixedDBkey();
-                                               $this->parser->mHeadings[] = array( $titleText, $headingIndex );
-                                               $serial = count( $this->parser->mHeadings ) - 1;
-                                               $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
-                                               $count = $contextNode->getAttribute( 'level' );
-                                               $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
-                                               $this->parser->mStripState->general->setPair( $marker, '' );
-                                       }
-                                       $out .= $s;
-                               } else {
-                                       # Generic recursive expansion
-                                       $newIterator = $contextNode->childNodes;
-                               }
-                       } else {
-                               throw new MWException( __METHOD__.': Invalid parameter type' );
-                       }
-
-                       if ( $newIterator !== false ) {
-                               if ( $newIterator instanceof PPNode_DOM ) {
-                                       $newIterator = $newIterator->node;
-                               }
-                               $outStack[] = '';
-                               $iteratorStack[] = $newIterator;
-                               $indexStack[] = 0;
-                       } elseif ( $iteratorStack[$level] === false ) {
-                               // Return accumulated value to parent
-                               // With tail recursion
-                               while ( $iteratorStack[$level] === false && $level > 0 ) {
-                                       $outStack[$level - 1] .= $out;
-                                       array_pop( $outStack );
-                                       array_pop( $iteratorStack );
-                                       array_pop( $indexStack );
-                                       $level--;
-                               }
-                       }
-               }
-               --$depth;
-               return $outStack[0];
-       }
-
-       function implodeWithFlags( $sep, $flags /*, ... */ ) {
-               $args = array_slice( func_get_args(), 2 );
-
-               $first = true;
-               $s = '';
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_DOM ) $root = $root->node;
-                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $s .= $sep;
-                               }
-                               $s .= $this->expand( $node, $flags );
-                       }
-               }
-               return $s;
-       }
-
-       /**
-        * Implode with no flags specified
-        * This previously called implodeWithFlags but has now been inlined to reduce stack depth
-        */
-       function implode( $sep /*, ... */ ) {
-               $args = array_slice( func_get_args(), 1 );
-
-               $first = true;
-               $s = '';
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_DOM ) $root = $root->node;
-                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $s .= $sep;
-                               }
-                               $s .= $this->expand( $node );
-                       }
-               }
-               return $s;
-       }
-
-       /**
-        * Makes an object that, when expand()ed, will be the same as one obtained
-        * with implode()
-        */
-       function virtualImplode( $sep /*, ... */ ) {
-               $args = array_slice( func_get_args(), 1 );
-               $out = array();
-               $first = true;
-               if ( $root instanceof PPNode_DOM ) $root = $root->node;
-
-               foreach ( $args as $root ) {
-                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $out[] = $sep;
-                               }
-                               $out[] = $node;
-                       }
-               }
-               return $out;
-       }
-
-       /**
-        * Virtual implode with brackets
-        */
-       function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
-               $args = array_slice( func_get_args(), 3 );
-               $out = array( $start );
-               $first = true;
-
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_DOM ) $root = $root->node;
-                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $out[] = $sep;
-                               }
-                               $out[] = $node;
-                       }
-               }
-               $out[] = $end;
-               return $out;
-       }
-
-       function __toString() {
-               return 'frame{}';
-       }
-
-       function getPDBK( $level = false ) {
-               if ( $level === false ) {
-                       return $this->title->getPrefixedDBkey();
-               } else {
-                       return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
-               }
-       }
-
-       /**
-        * Returns true if there are no arguments in this frame
-        */
-       function isEmpty() {
-               return true;
-       }
-
-       function getArgument( $name ) {
-               return false;
-       }
-
-       /**
-        * Returns true if the infinite loop check is OK, false if a loop is detected
-        */
-       function loopCheck( $title ) {
-               return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
-       }
-
-       /**
-        * Return true if the frame is a template frame
-        */
-       function isTemplate() {
-               return false;
-       }
-}
-
-/**
- * Expansion frame with template arguments
- * @ingroup Parser
- */
-class PPTemplateFrame_DOM extends PPFrame_DOM {
-       var $numberedArgs, $namedArgs, $parent;
-       var $numberedExpansionCache, $namedExpansionCache;
-
-       function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
-               $this->preprocessor = $preprocessor;
-               $this->parser = $preprocessor->parser;
-               $this->parent = $parent;
-               $this->numberedArgs = $numberedArgs;
-               $this->namedArgs = $namedArgs;
-               $this->title = $title;
-               $pdbk = $title ? $title->getPrefixedDBkey() : false;
-               $this->titleCache = $parent->titleCache;
-               $this->titleCache[] = $pdbk;
-               $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
-               if ( $pdbk !== false ) {
-                       $this->loopCheckHash[$pdbk] = true;
-               }
-               $this->depth = $parent->depth + 1;
-               $this->numberedExpansionCache = $this->namedExpansionCache = array();
-       }
-
-       function __toString() {
-               $s = 'tplframe{';
-               $first = true;
-               $args = $this->numberedArgs + $this->namedArgs;
-               foreach ( $args as $name => $value ) {
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $s .= ', ';
-                       }
-                       $s .= "\"$name\":\"" .
-                               str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
-               }
-               $s .= '}';
-               return $s;
-       }
-       /**
-        * Returns true if there are no arguments in this frame
-        */
-       function isEmpty() {
-               return !count( $this->numberedArgs ) && !count( $this->namedArgs );
-       }
-
-       function getNumberedArgument( $index ) {
-               if ( !isset( $this->numberedArgs[$index] ) ) {
-                       return false;
-               }
-               if ( !isset( $this->numberedExpansionCache[$index] ) ) {
-                       # No trimming for unnamed arguments
-                       $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
-               }
-               return $this->numberedExpansionCache[$index];
-       }
-
-       function getNamedArgument( $name ) {
-               if ( !isset( $this->namedArgs[$name] ) ) {
-                       return false;
-               }
-               if ( !isset( $this->namedExpansionCache[$name] ) ) {
-                       # Trim named arguments post-expand, for backwards compatibility
-                       $this->namedExpansionCache[$name] = trim(
-                               $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
-               }
-               return $this->namedExpansionCache[$name];
-       }
-
-       function getArgument( $name ) {
-               $text = $this->getNumberedArgument( $name );
-               if ( $text === false ) {
-                       $text = $this->getNamedArgument( $name );
-               }
-               return $text;
-       }
-
-       /**
-        * Return true if the frame is a template frame
-        */
-       function isTemplate() {
-               return true;
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_DOM implements PPNode {
-       var $node;
-
-       function __construct( $node, $xpath = false ) {
-               $this->node = $node;
-       }
-
-       function __get( $name ) {
-               if ( $name == 'xpath' ) {
-                       $this->xpath = new DOMXPath( $this->node->ownerDocument );
-               }
-               return $this->xpath;
-       }
-
-       function __toString() {
-               if ( $this->node instanceof DOMNodeList ) {
-                       $s = '';
-                       foreach ( $this->node as $node ) {
-                               $s .= $node->ownerDocument->saveXML( $node );
-                       }
-               } else {
-                       $s = $this->node->ownerDocument->saveXML( $this->node );
-               }
-               return $s;
-       }
-
-       function getChildren() {
-               return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
-       }
-
-       function getFirstChild() {
-               return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
-       }
-
-       function getNextSibling() {
-               return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
-       }
-
-       function getChildrenOfType( $type ) {
-               return new self( $this->xpath->query( $type, $this->node ) );
-       }
-
-       function getLength() {
-               if ( $this->node instanceof DOMNodeList ) {
-                       return $this->node->length;
-               } else {
-                       return false;
-               }
-       }
-
-       function item( $i ) {
-               $item = $this->node->item( $i );
-               return $item ? new self( $item ) : false;
-       }
-
-       function getName() {
-               if ( $this->node instanceof DOMNodeList ) {
-                       return '#nodelist';
-               } else {
-                       return $this->node->nodeName;
-               }
-       }
-
-       /**
-        * Split a <part> node into an associative array containing:
-        *    name          PPNode name
-        *    index         String index
-        *    value         PPNode value
-        */
-       function splitArg() {
-               $names = $this->xpath->query( 'name', $this->node );
-               $values = $this->xpath->query( 'value', $this->node );
-               if ( !$names->length || !$values->length ) {
-                       throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
-               }
-               $name = $names->item( 0 );
-               $index = $name->getAttribute( 'index' );
-               return array(
-                       'name' => new self( $name ),
-                       'index' => $index,
-                       'value' => new self( $values->item( 0 ) ) );
-       }
-
-       /**
-        * Split an <ext> node into an associative array containing name, attr, inner and close
-        * All values in the resulting array are PPNodes. Inner and close are optional.
-        */
-       function splitExt() {
-               $names = $this->xpath->query( 'name', $this->node );
-               $attrs = $this->xpath->query( 'attr', $this->node );
-               $inners = $this->xpath->query( 'inner', $this->node );
-               $closes = $this->xpath->query( 'close', $this->node );
-               if ( !$names->length || !$attrs->length ) {
-                       throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
-               }
-               $parts = array(
-                       'name' => new self( $names->item( 0 ) ),
-                       'attr' => new self( $attrs->item( 0 ) ) );
-               if ( $inners->length ) {
-                       $parts['inner'] = new self( $inners->item( 0 ) );
-               }
-               if ( $closes->length ) {
-                       $parts['close'] = new self( $closes->item( 0 ) );
-               }
-               return $parts;
-       }
-
-       /**
-        * Split a <h> node
-        */
-       function splitHeading() {
-               if ( !$this->nodeName == 'h' ) {
-                       throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
-               }
-               return array(
-                       'i' => $this->node->getAttribute( 'i' ),
-                       'level' => $this->node->getAttribute( 'level' ),
-                       'contents' => $this->getChildren()
-               );
-       }
-}
diff --git a/includes/Preprocessor_Hash.php b/includes/Preprocessor_Hash.php
deleted file mode 100644 (file)
index dc33788..0000000
+++ /dev/null
@@ -1,1497 +0,0 @@
-<?php
-
-/**
- * Differences from DOM schema:
- *   * attribute nodes are children
- *   * <h> nodes that aren't at the top are replaced with <possible-h>
- * @ingroup Parser
- */
-class Preprocessor_Hash implements Preprocessor {
-       var $parser;
-
-       function __construct( $parser ) {
-               $this->parser = $parser;
-       }
-
-       function newFrame() {
-               return new PPFrame_Hash( $this );
-       }
-
-       /**
-        * Preprocess some wikitext and return the document tree.
-        * This is the ghost of Parser::replace_variables().
-        *
-        * @param string $text The text to parse
-        * @param integer flags Bitwise combination of:
-        *          Parser::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
-        *                                     included. Default is to assume a direct page view.
-        *
-        * The generated DOM tree must depend only on the input text and the flags.
-        * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
-        *
-        * Any flag added to the $flags parameter here, or any other parameter liable to cause a
-        * change in the DOM tree for a given text, must be passed through the section identifier
-        * in the section edit link and thus back to extractSections().
-        *
-        * The output of this function is currently only cached in process memory, but a persistent
-        * cache may be implemented at a later date which takes further advantage of these strict
-        * dependency requirements.
-        *
-        * @private
-        */
-       function preprocessToObj( $text, $flags = 0 ) {
-               wfProfileIn( __METHOD__ );
-
-               $rules = array(
-                       '{' => array(
-                               'end' => '}',
-                               'names' => array(
-                                       2 => 'template',
-                                       3 => 'tplarg',
-                               ),
-                               'min' => 2,
-                               'max' => 3,
-                       ),
-                       '[' => array(
-                               'end' => ']',
-                               'names' => array( 2 => null ),
-                               'min' => 2,
-                               'max' => 2,
-                       )
-               );
-
-               $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
-
-               $xmlishElements = $this->parser->getStripList();
-               $enableOnlyinclude = false;
-               if ( $forInclusion ) {
-                       $ignoredTags = array( 'includeonly', '/includeonly' );
-                       $ignoredElements = array( 'noinclude' );
-                       $xmlishElements[] = 'noinclude';
-                       if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
-                               $enableOnlyinclude = true;
-                       }
-               } else {
-                       $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
-                       $ignoredElements = array( 'includeonly' );
-                       $xmlishElements[] = 'includeonly';
-               }
-               $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
-
-               // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
-               $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
-
-               $stack = new PPDStack_Hash;
-
-               $searchBase = "[{<\n";
-               $revText = strrev( $text ); // For fast reverse searches
-
-               $i = 0;                     # Input pointer, starts out pointing to a pseudo-newline before the start
-               $accum =& $stack->getAccum();   # Current accumulator
-               $findEquals = false;            # True to find equals signs in arguments
-               $findPipe = false;              # True to take notice of pipe characters
-               $headingIndex = 1;
-               $inHeading = false;        # True if $i is inside a possible heading
-               $noMoreGT = false;         # True if there are no more greater-than (>) signs right of $i
-               $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
-               $fakeLineStart = true;     # Do a line-start run without outputting an LF character
-
-               while ( true ) {
-                       //$this->memCheck();
-
-                       if ( $findOnlyinclude ) {
-                               // Ignore all input up to the next <onlyinclude>
-                               $startPos = strpos( $text, '<onlyinclude>', $i );
-                               if ( $startPos === false ) {
-                                       // Ignored section runs to the end
-                                       $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
-                                       break;
-                               }
-                               $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
-                               $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
-                               $i = $tagEndPos;
-                               $findOnlyinclude = false;
-                       }
-
-                       if ( $fakeLineStart ) {
-                               $found = 'line-start';
-                               $curChar = '';
-                       } else {
-                               # Find next opening brace, closing brace or pipe
-                               $search = $searchBase;
-                               if ( $stack->top === false ) {
-                                       $currentClosing = '';
-                               } else {
-                                       $currentClosing = $stack->top->close;
-                                       $search .= $currentClosing;
-                               }
-                               if ( $findPipe ) {
-                                       $search .= '|';
-                               }
-                               if ( $findEquals ) {
-                                       // First equals will be for the template
-                                       $search .= '=';
-                               }
-                               $rule = null;
-                               # Output literal section, advance input counter
-                               $literalLength = strcspn( $text, $search, $i );
-                               if ( $literalLength > 0 ) {
-                                       $accum->addLiteral( substr( $text, $i, $literalLength ) );
-                                       $i += $literalLength;
-                               }
-                               if ( $i >= strlen( $text ) ) {
-                                       if ( $currentClosing == "\n" ) {
-                                               // Do a past-the-end run to finish off the heading
-                                               $curChar = '';
-                                               $found = 'line-end';
-                                       } else {
-                                               # All done
-                                               break;
-                                       }
-                               } else {
-                                       $curChar = $text[$i];
-                                       if ( $curChar == '|' ) {
-                                               $found = 'pipe';
-                                       } elseif ( $curChar == '=' ) {
-                                               $found = 'equals';
-                                       } elseif ( $curChar == '<' ) {
-                                               $found = 'angle';
-                                       } elseif ( $curChar == "\n" ) {
-                                               if ( $inHeading ) {
-                                                       $found = 'line-end';
-                                               } else {
-                                                       $found = 'line-start';
-                                               }
-                                       } elseif ( $curChar == $currentClosing ) {
-                                               $found = 'close';
-                                       } elseif ( isset( $rules[$curChar] ) ) {
-                                               $found = 'open';
-                                               $rule = $rules[$curChar];
-                                       } else {
-                                               # Some versions of PHP have a strcspn which stops on null characters
-                                               # Ignore and continue
-                                               ++$i;
-                                               continue;
-                                       }
-                               }
-                       }
-
-                       if ( $found == 'angle' ) {
-                               $matches = false;
-                               // Handle </onlyinclude>
-                               if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
-                                       $findOnlyinclude = true;
-                                       continue;
-                               }
-
-                               // Determine element name
-                               if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
-                                       // Element name missing or not listed
-                                       $accum->addLiteral( '<' );
-                                       ++$i;
-                                       continue;
-                               }
-                               // Handle comments
-                               if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
-                                       // To avoid leaving blank lines, when a comment is both preceded
-                                       // and followed by a newline (ignoring spaces), trim leading and
-                                       // trailing spaces and one of the newlines.
-
-                                       // Find the end
-                                       $endPos = strpos( $text, '-->', $i + 4 );
-                                       if ( $endPos === false ) {
-                                               // Unclosed comment in input, runs to end
-                                               $inner = substr( $text, $i );
-                                               $accum->addNodeWithText( 'comment', $inner );
-                                               $i = strlen( $text );
-                                       } else {
-                                               // Search backwards for leading whitespace
-                                               $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
-                                               // Search forwards for trailing whitespace
-                                               // $wsEnd will be the position of the last space
-                                               $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
-                                               // Eat the line if possible
-                                               // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
-                                               // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
-                                               // it's a possible beneficial b/c break.
-                                               if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
-                                                       && substr( $text, $wsEnd + 1, 1 ) == "\n" )
-                                               {
-                                                       $startPos = $wsStart;
-                                                       $endPos = $wsEnd + 1;
-                                                       // Remove leading whitespace from the end of the accumulator
-                                                       // Sanity check first though
-                                                       $wsLength = $i - $wsStart;
-                                                       if ( $wsLength > 0
-                                                               && $accum->lastNode instanceof PPNode_Hash_Text
-                                                               && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
-                                                       {
-                                                               $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
-                                                       }
-                                                       // Do a line-start run next time to look for headings after the comment
-                                                       $fakeLineStart = true;
-                                               } else {
-                                                       // No line to eat, just take the comment itself
-                                                       $startPos = $i;
-                                                       $endPos += 2;
-                                               }
-
-                                               if ( $stack->top ) {
-                                                       $part = $stack->top->getCurrentPart();
-                                                       if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
-                                                               // Comments abutting, no change in visual end
-                                                               $part->commentEnd = $wsEnd;
-                                                       } else {
-                                                               $part->visualEnd = $wsStart;
-                                                               $part->commentEnd = $endPos;
-                                                       }
-                                               }
-                                               $i = $endPos + 1;
-                                               $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
-                                               $accum->addNodeWithText( 'comment', $inner );
-                                       }
-                                       continue;
-                               }
-                               $name = $matches[1];
-                               $lowerName = strtolower( $name );
-                               $attrStart = $i + strlen( $name ) + 1;
-
-                               // Find end of tag
-                               $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
-                               if ( $tagEndPos === false ) {
-                                       // Infinite backtrack
-                                       // Disable tag search to prevent worst-case O(N^2) performance
-                                       $noMoreGT = true;
-                                       $accum->addLiteral( '<' );
-                                       ++$i;
-                                       continue;
-                               }
-
-                               // Handle ignored tags
-                               if ( in_array( $lowerName, $ignoredTags ) ) {
-                                       $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
-                                       $i = $tagEndPos + 1;
-                                       continue;
-                               }
-
-                               $tagStartPos = $i;
-                               if ( $text[$tagEndPos-1] == '/' ) {
-                                       // Short end tag
-                                       $attrEnd = $tagEndPos - 1;
-                                       $inner = null;
-                                       $i = $tagEndPos + 1;
-                                       $close = null;
-                               } else {
-                                       $attrEnd = $tagEndPos;
-                                       // Find closing tag
-                                       if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
-                                               $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
-                                               $i = $matches[0][1] + strlen( $matches[0][0] );
-                                               $close = $matches[0][0];
-                                       } else {
-                                               // No end tag -- let it run out to the end of the text.
-                                               $inner = substr( $text, $tagEndPos + 1 );
-                                               $i = strlen( $text );
-                                               $close = null;
-                                       }
-                               }
-                               // <includeonly> and <noinclude> just become <ignore> tags
-                               if ( in_array( $lowerName, $ignoredElements ) ) {
-                                       $accum->addNodeWithText(  'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
-                                       continue;
-                               }
-
-                               if ( $attrEnd <= $attrStart ) {
-                                       $attr = '';
-                               } else {
-                                       // Note that the attr element contains the whitespace between name and attribute,
-                                       // this is necessary for precise reconstruction during pre-save transform.
-                                       $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
-                               }
-
-                               $extNode = new PPNode_Hash_Tree( 'ext' );
-                               $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
-                               $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
-                               if ( $inner !== null ) {
-                                       $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
-                               }
-                               if ( $close !== null ) {
-                                       $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
-                               }
-                               $accum->addNode( $extNode );
-                       }
-
-                       elseif ( $found == 'line-start' ) {
-                               // Is this the start of a heading?
-                               // Line break belongs before the heading element in any case
-                               if ( $fakeLineStart ) {
-                                       $fakeLineStart = false;
-                               } else {
-                                       $accum->addLiteral( $curChar );
-                                       $i++;
-                               }
-
-                               $count = strspn( $text, '=', $i, 6 );
-                               if ( $count == 1 && $findEquals ) {
-                                       // DWIM: This looks kind of like a name/value separator
-                                       // Let's let the equals handler have it and break the potential heading
-                                       // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
-                               } elseif ( $count > 0 ) {
-                                       $piece = array(
-                                               'open' => "\n",
-                                               'close' => "\n",
-                                               'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ),
-                                               'startPos' => $i,
-                                               'count' => $count );
-                                       $stack->push( $piece );
-                                       $accum =& $stack->getAccum();
-                                       extract( $stack->getFlags() );
-                                       $i += $count;
-                               }
-                       }
-
-                       elseif ( $found == 'line-end' ) {
-                               $piece = $stack->top;
-                               // A heading must be open, otherwise \n wouldn't have been in the search list
-                               assert( $piece->open == "\n" );
-                               $part = $piece->getCurrentPart();
-                               // Search back through the input to see if it has a proper close
-                               // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
-                               $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
-                               $searchStart = $i - $wsLength;
-                               if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
-                                       // Comment found at line end
-                                       // Search for equals signs before the comment
-                                       $searchStart = $part->visualEnd;
-                                       $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
-                               }
-                               $count = $piece->count;
-                               $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
-                               if ( $equalsLength > 0 ) {
-                                       if ( $i - $equalsLength == $piece->startPos ) {
-                                               // This is just a single string of equals signs on its own line
-                                               // Replicate the doHeadings behaviour /={count}(.+)={count}/
-                                               // First find out how many equals signs there really are (don't stop at 6)
-                                               $count = $equalsLength;
-                                               if ( $count < 3 ) {
-                                                       $count = 0;
-                                               } else {
-                                                       $count = min( 6, intval( ( $count - 1 ) / 2 ) );
-                                               }
-                                       } else {
-                                               $count = min( $equalsLength, $count );
-                                       }
-                                       if ( $count > 0 ) {
-                                               // Normal match, output <h>
-                                               $element = new PPNode_Hash_Tree( 'possible-h' );
-                                               $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
-                                               $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
-                                               $element->lastChild->nextSibling = $accum->firstNode;
-                                               $element->lastChild = $accum->lastNode;
-                                       } else {
-                                               // Single equals sign on its own line, count=0
-                                               $element = $accum;
-                                       }
-                               } else {
-                                       // No match, no <h>, just pass down the inner text
-                                       $element = $accum;
-                               }
-                               // Unwind the stack
-                               $stack->pop();
-                               $accum =& $stack->getAccum();
-                               extract( $stack->getFlags() );
-
-                               // Append the result to the enclosing accumulator
-                               if ( $element instanceof PPNode ) {
-                                       $accum->addNode( $element );
-                               } else {
-                                       $accum->addAccum( $element );
-                               }
-                               // Note that we do NOT increment the input pointer.
-                               // This is because the closing linebreak could be the opening linebreak of
-                               // another heading. Infinite loops are avoided because the next iteration MUST
-                               // hit the heading open case above, which unconditionally increments the
-                               // input pointer.
-                       }
-
-                       elseif ( $found == 'open' ) {
-                               # count opening brace characters
-                               $count = strspn( $text, $curChar, $i );
-
-                               # we need to add to stack only if opening brace count is enough for one of the rules
-                               if ( $count >= $rule['min'] ) {
-                                       # Add it to the stack
-                                       $piece = array(
-                                               'open' => $curChar,
-                                               'close' => $rule['end'],
-                                               'count' => $count,
-                                               'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
-                                       );
-
-                                       $stack->push( $piece );
-                                       $accum =& $stack->getAccum();
-                                       extract( $stack->getFlags() );
-                               } else {
-                                       # Add literal brace(s)
-                                       $accum->addLiteral( str_repeat( $curChar, $count ) );
-                               }
-                               $i += $count;
-                       }
-
-                       elseif ( $found == 'close' ) {
-                               $piece = $stack->top;
-                               # lets check if there are enough characters for closing brace
-                               $maxCount = $piece->count;
-                               $count = strspn( $text, $curChar, $i, $maxCount );
-
-                               # check for maximum matching characters (if there are 5 closing
-                               # characters, we will probably need only 3 - depending on the rules)
-                               $matchingCount = 0;
-                               $rule = $rules[$piece->open];
-                               if ( $count > $rule['max'] ) {
-                                       # The specified maximum exists in the callback array, unless the caller
-                                       # has made an error
-                                       $matchingCount = $rule['max'];
-                               } else {
-                                       # Count is less than the maximum
-                                       # Skip any gaps in the callback array to find the true largest match
-                                       # Need to use array_key_exists not isset because the callback can be null
-                                       $matchingCount = $count;
-                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
-                                               --$matchingCount;
-                                       }
-                               }
-
-                               if ($matchingCount <= 0) {
-                                       # No matching element found in callback array
-                                       # Output a literal closing brace and continue
-                                       $accum->addLiteral( str_repeat( $curChar, $count ) );
-                                       $i += $count;
-                                       continue;
-                               }
-                               $name = $rule['names'][$matchingCount];
-                               if ( $name === null ) {
-                                       // No element, just literal text
-                                       $element = $piece->breakSyntax( $matchingCount );
-                                       $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
-                               } else {
-                                       # Create XML element
-                                       # Note: $parts is already XML, does not need to be encoded further
-                                       $parts = $piece->parts;
-                                       $titleAccum = $parts[0]->out;
-                                       unset( $parts[0] );
-
-                                       $element = new PPNode_Hash_Tree( $name );
-
-                                       # The invocation is at the start of the line if lineStart is set in
-                                       # the stack, and all opening brackets are used up.
-                                       if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
-                                               $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
-                                       }
-                                       $titleNode = new PPNode_Hash_Tree( 'title' );
-                                       $titleNode->firstChild = $titleAccum->firstNode;
-                                       $titleNode->lastChild = $titleAccum->lastNode;
-                                       $element->addChild( $titleNode );
-                                       $argIndex = 1;
-                                       foreach ( $parts as $partIndex => $part ) {
-                                               if ( isset( $part->eqpos ) ) {
-                                                       // Find equals
-                                                       $lastNode = false;
-                                                       for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
-                                                               if ( $node === $part->eqpos ) {
-                                                                       break;
-                                                               }
-                                                               $lastNode = $node;
-                                                       }
-                                                       if ( !$node ) {
-                                                               throw new MWException( __METHOD__. ': eqpos not found' );
-                                                       }
-                                                       if ( $node->name !== 'equals' ) {
-                                                               throw new MWException( __METHOD__ .': eqpos is not equals' );
-                                                       }
-                                                       $equalsNode = $node;
-
-                                                       // Construct name node
-                                                       $nameNode = new PPNode_Hash_Tree( 'name' );
-                                                       if ( $lastNode !== false ) {
-                                                               $lastNode->nextSibling = false;
-                                                               $nameNode->firstChild = $part->out->firstNode;
-                                                               $nameNode->lastChild = $lastNode;
-                                                       }
-
-                                                       // Construct value node
-                                                       $valueNode = new PPNode_Hash_Tree( 'value' );
-                                                       if ( $equalsNode->nextSibling !== false ) {
-                                                               $valueNode->firstChild = $equalsNode->nextSibling;
-                                                               $valueNode->lastChild = $part->out->lastNode;
-                                                       }
-                                                       $partNode = new PPNode_Hash_Tree( 'part' );
-                                                       $partNode->addChild( $nameNode );
-                                                       $partNode->addChild( $equalsNode->firstChild );
-                                                       $partNode->addChild( $valueNode );
-                                                       $element->addChild( $partNode );
-                                               } else {
-                                                       $partNode = new PPNode_Hash_Tree( 'part' );
-                                                       $nameNode = new PPNode_Hash_Tree( 'name' );
-                                                       $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
-                                                       $valueNode = new PPNode_Hash_Tree( 'value' );
-                                                       $valueNode->firstChild = $part->out->firstNode;
-                                                       $valueNode->lastChild = $part->out->lastNode;
-                                                       $partNode->addChild( $nameNode );
-                                                       $partNode->addChild( $valueNode );
-                                                       $element->addChild( $partNode );
-                                               }
-                                       }
-                               }
-
-                               # Advance input pointer
-                               $i += $matchingCount;
-
-                               # Unwind the stack
-                               $stack->pop();
-                               $accum =& $stack->getAccum();
-
-                               # Re-add the old stack element if it still has unmatched opening characters remaining
-                               if ($matchingCount < $piece->count) {
-                                       $piece->parts = array( new PPDPart_Hash );
-                                       $piece->count -= $matchingCount;
-                                       # do we still qualify for any callback with remaining count?
-                                       $names = $rules[$piece->open]['names'];
-                                       $skippedBraces = 0;
-                                       $enclosingAccum =& $accum;
-                                       while ( $piece->count ) {
-                                               if ( array_key_exists( $piece->count, $names ) ) {
-                                                       $stack->push( $piece );
-                                                       $accum =& $stack->getAccum();
-                                                       break;
-                                               }
-                                               --$piece->count;
-                                               $skippedBraces ++;
-                                       }
-                                       $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
-                               }
-
-                               extract( $stack->getFlags() );
-
-                               # Add XML element to the enclosing accumulator
-                               if ( $element instanceof PPNode ) {
-                                       $accum->addNode( $element );
-                               } else {
-                                       $accum->addAccum( $element );
-                               }
-                       }
-
-                       elseif ( $found == 'pipe' ) {
-                               $findEquals = true; // shortcut for getFlags()
-                               $stack->addPart();
-                               $accum =& $stack->getAccum();
-                               ++$i;
-                       }
-
-                       elseif ( $found == 'equals' ) {
-                               $findEquals = false; // shortcut for getFlags()
-                               $accum->addNodeWithText( 'equals', '=' );
-                               $stack->getCurrentPart()->eqpos = $accum->lastNode;
-                               ++$i;
-                       }
-               }
-
-               # Output any remaining unclosed brackets
-               foreach ( $stack->stack as $piece ) {
-                       $stack->rootAccum->addAccum( $piece->breakSyntax() );
-               }
-
-               # Enable top-level headings
-               for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
-                       if ( isset( $node->name ) && $node->name === 'possible-h' ) {
-                               $node->name = 'h';
-                       }
-               }
-
-               $rootNode = new PPNode_Hash_Tree( 'root' );
-               $rootNode->firstChild = $stack->rootAccum->firstNode;
-               $rootNode->lastChild = $stack->rootAccum->lastNode;
-               wfProfileOut( __METHOD__ );
-               return $rootNode;
-       }
-}
-
-/**
- * Stack class to help Preprocessor::preprocessToObj()
- * @ingroup Parser
- */
-class PPDStack_Hash extends PPDStack {
-       function __construct() {
-               $this->elementClass = 'PPDStackElement_Hash';
-               parent::__construct();
-               $this->rootAccum = new PPDAccum_Hash;
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDStackElement_Hash extends PPDStackElement {
-       function __construct( $data = array() ) {
-               $this->partClass = 'PPDPart_Hash';
-               parent::__construct( $data );
-       }
-
-       /**
-        * Get the accumulator that would result if the close is not found.
-        */
-       function breakSyntax( $openingCount = false ) {
-               if ( $this->open == "\n" ) {
-                       $accum = $this->parts[0]->out;
-               } else {
-                       if ( $openingCount === false ) {
-                               $openingCount = $this->count;
-                       }
-                       $accum = new PPDAccum_Hash;
-                       $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
-                       $first = true;
-                       foreach ( $this->parts as $part ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $accum->addLiteral( '|' );
-                               }
-                               $accum->addAccum( $part->out );
-                       }
-               }
-               return $accum;
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDPart_Hash extends PPDPart {
-       function __construct( $out = '' ) {
-               $accum = new PPDAccum_Hash;
-               if ( $out !== '' ) {
-                       $accum->addLiteral( $out );
-               }
-               parent::__construct( $accum );
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPDAccum_Hash {
-       var $firstNode, $lastNode;
-
-       function __construct() {
-               $this->firstNode = $this->lastNode = false;
-       }
-
-       /**
-        * Append a string literal
-        */
-       function addLiteral( $s ) {
-               if ( $this->lastNode === false ) {
-                       $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
-               } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
-                       $this->lastNode->value .= $s;
-               } else {
-                       $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
-                       $this->lastNode = $this->lastNode->nextSibling;
-               }
-       }
-
-       /**
-        * Append a PPNode
-        */
-       function addNode( PPNode $node ) {
-               if ( $this->lastNode === false ) {
-                       $this->firstNode = $this->lastNode = $node;
-               } else {
-                       $this->lastNode->nextSibling = $node;
-                       $this->lastNode = $node;
-               }
-       }
-
-       /**
-        * Append a tree node with text contents
-        */
-       function addNodeWithText( $name, $value ) {
-               $node = PPNode_Hash_Tree::newWithText( $name, $value );
-               $this->addNode( $node );
-       }
-
-       /**
-        * Append a PPAccum_Hash
-        * Takes over ownership of the nodes in the source argument. These nodes may
-        * subsequently be modified, especially nextSibling.
-        */
-       function addAccum( $accum ) {
-               if ( $accum->lastNode === false ) {
-                       // nothing to add
-               } elseif ( $this->lastNode === false ) {
-                       $this->firstNode = $accum->firstNode;
-                       $this->lastNode = $accum->lastNode;
-               } else {
-                       $this->lastNode->nextSibling = $accum->firstNode;
-                       $this->lastNode = $accum->lastNode;
-               }
-       }
-}
-
-/**
- * An expansion frame, used as a context to expand the result of preprocessToObj()
- * @ingroup Parser
- */
-class PPFrame_Hash implements PPFrame {
-       var $preprocessor, $parser, $title;
-       var $titleCache;
-
-       /**
-        * Hashtable listing templates which are disallowed for expansion in this frame,
-        * having been encountered previously in parent frames.
-        */
-       var $loopCheckHash;
-
-       /**
-        * Recursion depth of this frame, top = 0
-        */
-       var $depth;
-
-
-       /**
-        * Construct a new preprocessor frame.
-        * @param Preprocessor $preprocessor The parent preprocessor
-        */
-       function __construct( $preprocessor ) {
-               $this->preprocessor = $preprocessor;
-               $this->parser = $preprocessor->parser;
-               $this->title = $this->parser->mTitle;
-               $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
-               $this->loopCheckHash = array();
-               $this->depth = 0;
-       }
-
-       /**
-        * Create a new child frame
-        * $args is optionally a multi-root PPNode or array containing the template arguments
-        */
-       function newChild( $args = false, $title = false ) {
-               $namedArgs = array();
-               $numberedArgs = array();
-               if ( $title === false ) {
-                       $title = $this->title;
-               }
-               if ( $args !== false ) {
-                       $xpath = false;
-                       if ( $args instanceof PPNode_Hash_Array ) {
-                               $args = $args->value;
-                       } elseif ( !is_array( $args ) ) {
-                               throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
-                       }
-                       foreach ( $args as $arg ) {
-                               $bits = $arg->splitArg();
-                               if ( $bits['index'] !== '' ) {
-                                       // Numbered parameter
-                                       $numberedArgs[$bits['index']] = $bits['value'];
-                                       unset( $namedArgs[$bits['index']] );
-                               } else {
-                                       // Named parameter
-                                       $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
-                                       $namedArgs[$name] = $bits['value'];
-                                       unset( $numberedArgs[$name] );
-                               }
-                       }
-               }
-               return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
-       }
-
-       function expand( $root, $flags = 0 ) {
-               if ( is_string( $root ) ) {
-                       return $root;
-               }
-
-               if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
-               {
-                       return '<span class="error">Node-count limit exceeded</span>';
-               }
-               if ( $this->depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
-                       return '<span class="error">Expansion depth limit exceeded</span>';
-               }
-               ++$this->depth;
-
-               $outStack = array( '', '' );
-               $iteratorStack = array( false, $root );
-               $indexStack = array( 0, 0 );
-
-               while ( count( $iteratorStack ) > 1 ) {
-                       $level = count( $outStack ) - 1;
-                       $iteratorNode =& $iteratorStack[ $level ];
-                       $out =& $outStack[$level];
-                       $index =& $indexStack[$level];
-
-                       if ( is_array( $iteratorNode ) ) {
-                               if ( $index >= count( $iteratorNode ) ) {
-                                       // All done with this iterator
-                                       $iteratorStack[$level] = false;
-                                       $contextNode = false;
-                               } else {
-                                       $contextNode = $iteratorNode[$index];
-                                       $index++;
-                               }
-                       } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
-                               if ( $index >= $iteratorNode->getLength() ) {
-                                       // All done with this iterator
-                                       $iteratorStack[$level] = false;
-                                       $contextNode = false;
-                               } else {
-                                       $contextNode = $iteratorNode->item( $index );
-                                       $index++;
-                               }
-                       } else {
-                               // Copy to $contextNode and then delete from iterator stack,
-                               // because this is not an iterator but we do have to execute it once
-                               $contextNode = $iteratorStack[$level];
-                               $iteratorStack[$level] = false;
-                       }
-
-                       $newIterator = false;
-
-                       if ( $contextNode === false ) {
-                               // nothing to do
-                       } elseif ( is_string( $contextNode ) ) {
-                               $out .= $contextNode;
-                       } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
-                               $newIterator = $contextNode;
-                       } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
-                               // No output
-                       } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
-                               $out .= $contextNode->value;
-                       } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
-                               if ( $contextNode->name == 'template' ) {
-                                       # Double-brace expansion
-                                       $bits = $contextNode->splitTemplate();
-                                       if ( $flags & self::NO_TEMPLATES ) {
-                                               $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
-                                       } else {
-                                               $ret = $this->parser->braceSubstitution( $bits, $this );
-                                               if ( isset( $ret['object'] ) ) {
-                                                       $newIterator = $ret['object'];
-                                               } else {
-                                                       $out .= $ret['text'];
-                                               }
-                                       }
-                               } elseif ( $contextNode->name == 'tplarg' ) {
-                                       # Triple-brace expansion
-                                       $bits = $contextNode->splitTemplate();
-                                       if ( $flags & self::NO_ARGS ) {
-                                               $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
-                                       } else {
-                                               $ret = $this->parser->argSubstitution( $bits, $this );
-                                               if ( isset( $ret['object'] ) ) {
-                                                       $newIterator = $ret['object'];
-                                               } else {
-                                                       $out .= $ret['text'];
-                                               }
-                                       }
-                               } elseif ( $contextNode->name == 'comment' ) {
-                                       # HTML-style comment
-                                       # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
-                                       if ( $this->parser->ot['html']
-                                               || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
-                                               || ( $flags & self::STRIP_COMMENTS ) )
-                                       {
-                                               $out .= '';
-                                       }
-                                       # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
-                                       # Not in RECOVER_COMMENTS mode (extractSections) though
-                                       elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
-                                               $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
-                                       }
-                                       # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
-                                       else {
-                                               $out .= $contextNode->firstChild->value;
-                                       }
-                               } elseif ( $contextNode->name == 'ignore' ) {
-                                       # Output suppression used by <includeonly> etc.
-                                       # OT_WIKI will only respect <ignore> in substed templates.
-                                       # The other output types respect it unless NO_IGNORE is set.
-                                       # extractSections() sets NO_IGNORE and so never respects it.
-                                       if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
-                                               $out .= $contextNode->firstChild->value;
-                                       } else {
-                                               //$out .= '';
-                                       }
-                               } elseif ( $contextNode->name == 'ext' ) {
-                                       # Extension tag
-                                       $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
-                                       $out .= $this->parser->extensionSubstitution( $bits, $this );
-                               } elseif ( $contextNode->name == 'h' ) {
-                                       # Heading
-                                       if ( $this->parser->ot['html'] ) {
-                                               # Expand immediately and insert heading index marker
-                                               $s = '';
-                                               for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
-                                                       $s .= $this->expand( $node, $flags );
-                                               }
-
-                                               $bits = $contextNode->splitHeading();
-                                               $titleText = $this->title->getPrefixedDBkey();
-                                               $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
-                                               $serial = count( $this->parser->mHeadings ) - 1;
-                                               $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
-                                               $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
-                                               $this->parser->mStripState->general->setPair( $marker, '' );
-                                               $out .= $s;
-                                       } else {
-                                               # Expand in virtual stack
-                                               $newIterator = $contextNode->getChildren();
-                                       }
-                               } else {
-                                       # Generic recursive expansion
-                                       $newIterator = $contextNode->getChildren();
-                               }
-                       } else {
-                               throw new MWException( __METHOD__.': Invalid parameter type' );
-                       }
-
-                       if ( $newIterator !== false ) {
-                               $outStack[] = '';
-                               $iteratorStack[] = $newIterator;
-                               $indexStack[] = 0;
-                       } elseif ( $iteratorStack[$level] === false ) {
-                               // Return accumulated value to parent
-                               // With tail recursion
-                               while ( $iteratorStack[$level] === false && $level > 0 ) {
-                                       $outStack[$level - 1] .= $out;
-                                       array_pop( $outStack );
-                                       array_pop( $iteratorStack );
-                                       array_pop( $indexStack );
-                                       $level--;
-                               }
-                       }
-               }
-               --$this->depth;
-               return $outStack[0];
-       }
-
-       function implodeWithFlags( $sep, $flags /*, ... */ ) {
-               $args = array_slice( func_get_args(), 2 );
-
-               $first = true;
-               $s = '';
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_Hash_Array ) {
-                               $root = $root->value;
-                       }
-                       if ( !is_array( $root ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $s .= $sep;
-                               }
-                               $s .= $this->expand( $node, $flags );
-                       }
-               }
-               return $s;
-       }
-
-       /**
-        * Implode with no flags specified
-        * This previously called implodeWithFlags but has now been inlined to reduce stack depth
-        */
-       function implode( $sep /*, ... */ ) {
-               $args = array_slice( func_get_args(), 1 );
-
-               $first = true;
-               $s = '';
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_Hash_Array ) {
-                               $root = $root->value;
-                       }
-                       if ( !is_array( $root ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $s .= $sep;
-                               }
-                               $s .= $this->expand( $node );
-                       }
-               }
-               return $s;
-       }
-
-       /**
-        * Makes an object that, when expand()ed, will be the same as one obtained
-        * with implode()
-        */
-       function virtualImplode( $sep /*, ... */ ) {
-               $args = array_slice( func_get_args(), 1 );
-               $out = array();
-               $first = true;
-
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_Hash_Array ) {
-                               $root = $root->value;
-                       }
-                       if ( !is_array( $root ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $out[] = $sep;
-                               }
-                               $out[] = $node;
-                       }
-               }
-               return new PPNode_Hash_Array( $out );
-       }
-
-       /**
-        * Virtual implode with brackets
-        */
-       function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
-               $args = array_slice( func_get_args(), 3 );
-               $out = array( $start );
-               $first = true;
-
-               foreach ( $args as $root ) {
-                       if ( $root instanceof PPNode_Hash_Array ) {
-                               $root = $root->value;
-                       }
-                       if ( !is_array( $root ) ) {
-                               $root = array( $root );
-                       }
-                       foreach ( $root as $node ) {
-                               if ( $first ) {
-                                       $first = false;
-                               } else {
-                                       $out[] = $sep;
-                               }
-                               $out[] = $node;
-                       }
-               }
-               $out[] = $end;
-               return new PPNode_Hash_Array( $out );
-       }
-
-       function __toString() {
-               return 'frame{}';
-       }
-
-       function getPDBK( $level = false ) {
-               if ( $level === false ) {
-                       return $this->title->getPrefixedDBkey();
-               } else {
-                       return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
-               }
-       }
-
-       /**
-        * Returns true if there are no arguments in this frame
-        */
-       function isEmpty() {
-               return true;
-       }
-
-       function getArgument( $name ) {
-               return false;
-       }
-
-       /**
-        * Returns true if the infinite loop check is OK, false if a loop is detected
-        */
-       function loopCheck( $title ) {
-               return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
-       }
-
-       /**
-        * Return true if the frame is a template frame
-        */
-       function isTemplate() {
-               return false;
-       }
-}
-
-/**
- * Expansion frame with template arguments
- * @ingroup Parser
- */
-class PPTemplateFrame_Hash extends PPFrame_Hash {
-       var $numberedArgs, $namedArgs, $parent;
-       var $numberedExpansionCache, $namedExpansionCache;
-
-       function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
-               $this->preprocessor = $preprocessor;
-               $this->parser = $preprocessor->parser;
-               $this->parent = $parent;
-               $this->numberedArgs = $numberedArgs;
-               $this->namedArgs = $namedArgs;
-               $this->title = $title;
-               $pdbk = $title ? $title->getPrefixedDBkey() : false;
-               $this->titleCache = $parent->titleCache;
-               $this->titleCache[] = $pdbk;
-               $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
-               if ( $pdbk !== false ) {
-                       $this->loopCheckHash[$pdbk] = true;
-               }
-               $this->depth = $parent->depth + 1;
-               $this->numberedExpansionCache = $this->namedExpansionCache = array();
-       }
-
-       function __toString() {
-               $s = 'tplframe{';
-               $first = true;
-               $args = $this->numberedArgs + $this->namedArgs;
-               foreach ( $args as $name => $value ) {
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $s .= ', ';
-                       }
-                       $s .= "\"$name\":\"" .
-                               str_replace( '"', '\\"', $value->__toString() ) . '"';
-               }
-               $s .= '}';
-               return $s;
-       }
-       /**
-        * Returns true if there are no arguments in this frame
-        */
-       function isEmpty() {
-               return !count( $this->numberedArgs ) && !count( $this->namedArgs );
-       }
-
-       function getNumberedArgument( $index ) {
-               if ( !isset( $this->numberedArgs[$index] ) ) {
-                       return false;
-               }
-               if ( !isset( $this->numberedExpansionCache[$index] ) ) {
-                       # No trimming for unnamed arguments
-                       $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
-               }
-               return $this->numberedExpansionCache[$index];
-       }
-
-       function getNamedArgument( $name ) {
-               if ( !isset( $this->namedArgs[$name] ) ) {
-                       return false;
-               }
-               if ( !isset( $this->namedExpansionCache[$name] ) ) {
-                       # Trim named arguments post-expand, for backwards compatibility
-                       $this->namedExpansionCache[$name] = trim(
-                               $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
-               }
-               return $this->namedExpansionCache[$name];
-       }
-
-       function getArgument( $name ) {
-               $text = $this->getNumberedArgument( $name );
-               if ( $text === false ) {
-                       $text = $this->getNamedArgument( $name );
-               }
-               return $text;
-       }
-
-       /**
-        * Return true if the frame is a template frame
-        */
-       function isTemplate() {
-               return true;
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_Hash_Tree implements PPNode {
-       var $name, $firstChild, $lastChild, $nextSibling;
-
-       function __construct( $name ) {
-               $this->name = $name;
-               $this->firstChild = $this->lastChild = $this->nextSibling = false;
-       }
-
-       function __toString() {
-               $inner = '';
-               $attribs = '';
-               for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
-                       if ( $node instanceof PPNode_Hash_Attr ) {
-                               $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
-                       } else {
-                               $inner .= $node->__toString();
-                       }
-               }
-               if ( $inner === '' ) {
-                       return "<{$this->name}$attribs/>";
-               } else {
-                       return "<{$this->name}$attribs>$inner</{$this->name}>";
-               }
-       }
-
-       static function newWithText( $name, $text ) {
-               $obj = new self( $name );
-               $obj->addChild( new PPNode_Hash_Text( $text ) );
-               return $obj;
-       }
-
-       function addChild( $node ) {
-               if ( $this->lastChild === false ) {
-                       $this->firstChild = $this->lastChild = $node;
-               } else {
-                       $this->lastChild->nextSibling = $node;
-                       $this->lastChild = $node;
-               }
-       }
-
-       function getChildren() {
-               $children = array();
-               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
-                       $children[] = $child;
-               }
-               return new PPNode_Hash_Array( $children );
-       }
-
-       function getFirstChild() {
-               return $this->firstChild;
-       }
-
-       function getNextSibling() {
-               return $this->nextSibling;
-       }
-
-       function getChildrenOfType( $name ) {
-               $children = array();
-               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
-                       if ( isset( $child->name ) && $child->name === $name ) {
-                               $children[] = $name;
-                       }
-               }
-               return $children;
-       }
-
-       function getLength() { return false; }
-       function item( $i ) { return false; }
-
-       function getName() {
-               return $this->name;
-       }
-
-       /**
-        * Split a <part> node into an associative array containing:
-        *    name          PPNode name
-        *    index         String index
-        *    value         PPNode value
-        */
-       function splitArg() {
-               $bits = array();
-               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
-                       if ( !isset( $child->name ) ) {
-                               continue;
-                       }
-                       if ( $child->name === 'name' ) {
-                               $bits['name'] = $child;
-                               if ( $child->firstChild instanceof PPNode_Hash_Attr
-                                       && $child->firstChild->name === 'index' )
-                               {
-                                       $bits['index'] = $child->firstChild->value;
-                               }
-                       } elseif ( $child->name === 'value' ) {
-                               $bits['value'] = $child;
-                       }
-               }
-
-               if ( !isset( $bits['name'] ) ) {
-                       throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
-               }
-               if ( !isset( $bits['index'] ) ) {
-                       $bits['index'] = '';
-               }
-               return $bits;
-       }
-
-       /**
-        * Split an <ext> node into an associative array containing name, attr, inner and close
-        * All values in the resulting array are PPNodes. Inner and close are optional.
-        */
-       function splitExt() {
-               $bits = array();
-               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
-                       if ( !isset( $child->name ) ) {
-                               continue;
-                       }
-                       if ( $child->name == 'name' ) {
-                               $bits['name'] = $child;
-                       } elseif ( $child->name == 'attr' ) {
-                               $bits['attr'] = $child;
-                       } elseif ( $child->name == 'inner' ) {
-                               $bits['inner'] = $child;
-                       } elseif ( $child->name == 'close' ) {
-                               $bits['close'] = $child;
-                       }
-               }
-               if ( !isset( $bits['name'] ) ) {
-                       throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
-               }
-               return $bits;
-       }
-
-       /**
-        * Split an <h> node
-        */
-       function splitHeading() {
-               if ( $this->name !== 'h' ) {
-                       throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
-               }
-               $bits = array();
-               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
-                       if ( !isset( $child->name ) ) {
-                               continue;
-                       }
-                       if ( $child->name == 'i' ) {
-                               $bits['i'] = $child->value;
-                       } elseif ( $child->name == 'level' ) {
-                               $bits['level'] = $child->value;
-                       }
-               }
-               if ( !isset( $bits['i'] ) ) {
-                       throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
-               }
-               return $bits;
-       }
-
-       /**
-        * Split a <template> or <tplarg> node
-        */
-       function splitTemplate() {
-               $parts = array();
-               $bits = array( 'lineStart' => '' );
-               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
-                       if ( !isset( $child->name ) ) {
-                               continue;
-                       }
-                       if ( $child->name == 'title' ) {
-                               $bits['title'] = $child;
-                       }
-                       if ( $child->name == 'part' ) {
-                               $parts[] = $child;
-                       }
-                       if ( $child->name == 'lineStart' ) {
-                               $bits['lineStart'] = '1';
-                       }
-               }
-               if ( !isset( $bits['title'] ) ) {
-                       throw new MWException( 'Invalid node passed to ' . __METHOD__ );
-               }
-               $bits['parts'] = new PPNode_Hash_Array( $parts );
-               return $bits;
-       }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_Hash_Text implements PPNode {
-       var $value, $nextSibling;
-
-       function __construct( $value ) {
-               if ( is_object( $value ) ) {
-                       throw new MWException( __CLASS__ . ' given object instead of string' );
-               }
-               $this->value = $value;
-       }
-
-       function __toString() {
-               return htmlspecialchars( $this->value );
-       }
-
-       function getNextSibling() {
-               return $this->nextSibling;
-       }
-
-       function getChildren() { return false; }
-       function getFirstChild() { return false; }
-       function getChildrenOfType( $name ) { return false; }
-       function getLength() { return false; }
-       function item( $i ) { return false; }
-       function getName() { return '#text'; }
-       function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
-       function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
-       function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_Hash_Array implements PPNode {
-       var $value, $nextSibling;
-
-       function __construct( $value ) {
-               $this->value = $value;
-       }
-
-       function __toString() {
-               return var_export( $this, true );
-       }
-
-       function getLength() {
-               return count( $this->value );
-       }
-
-       function item( $i ) {
-               return $this->value[$i];
-       }
-
-       function getName() { return '#nodelist'; }
-
-       function getNextSibling() {
-               return $this->nextSibling;
-       }
-
-       function getChildren() { return false; }
-       function getFirstChild() { return false; }
-       function getChildrenOfType( $name ) { return false; }
-       function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
-       function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
-       function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
-}
-
-/**
- * @ingroup Parser
- */
-class PPNode_Hash_Attr implements PPNode {
-       var $name, $value, $nextSibling;
-
-       function __construct( $name, $value ) {
-               $this->name = $name;
-               $this->value = $value;
-       }
-
-       function __toString() {
-               return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
-       }
-
-       function getName() {
-               return $this->name;
-       }
-
-       function getNextSibling() {
-               return $this->nextSibling;
-       }
-
-       function getChildren() { return false; }
-       function getFirstChild() { return false; }
-       function getChildrenOfType( $name ) { return false; }
-       function getLength() { return false; }
-       function item( $i ) { return false; }
-       function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
-       function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
-       function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
-}
index 2ed4a41..c5d3f1b 100644 (file)
@@ -58,7 +58,6 @@ if ( empty( $wgFileStore['deleted']['directory'] ) ) {
        $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
 }
 
-
 /**
  * Initialise $wgLocalFileRepo from backwards-compatible settings
  */
@@ -115,7 +114,9 @@ if ( $wgUseSharedUploads ) {
        }
 }
 
-require_once( "$IP/includes/AutoLoader.php" );
+if ( !class_exists( 'AutoLoader' ) ) {
+       require_once( "$IP/includes/AutoLoader.php" );
+}
 
 wfProfileIn( $fname.'-exception' );
 require_once( "$IP/includes/Exception.php" );
diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php
deleted file mode 100644 (file)
index c2a8de4..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-<?php
-/**
- * Use this special page to get a list of the MediaWiki system messages.
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor.
- */
-function wfSpecialAllmessages() {
-       global $wgOut, $wgRequest, $wgMessageCache, $wgTitle;
-       global $wgUseDatabaseMessages;
-
-       # The page isn't much use if the MediaWiki namespace is not being used
-       if( !$wgUseDatabaseMessages ) {
-               $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
-               return;
-       }
-
-       wfProfileIn( __METHOD__ );
-
-       wfProfileIn( __METHOD__ . '-setup' );
-       $ot = $wgRequest->getText( 'ot' );
-
-       $navText = wfMsg( 'allmessagestext' );
-
-       # Make sure all extension messages are available
-
-       $wgMessageCache->loadAllMessages();
-
-       $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) );
-       ksort( $sortedArray );
-       $messages = array();
-
-       foreach ( $sortedArray as $key => $value ) {
-               $messages[$key]['enmsg'] = $value;
-               $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); // wfMsgNoDbNoTrans doesn't exist
-               $messages[$key]['msg'] = wfMsgNoTrans( $key );
-       }
-
-       wfProfileOut( __METHOD__ . '-setup' );
-
-       wfProfileIn( __METHOD__ . '-output' );
-       $wgOut->addScriptFile( 'allmessages.js' );
-       if ( $ot == 'php' ) {
-               $navText .= wfAllMessagesMakePhp( $messages );
-               $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a> | ' .
-                       '<a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' .
-                       '<pre>' . htmlspecialchars( $navText ) . '</pre>' );
-       } else if ( $ot == 'xml' ) {
-               $wgOut->disable();
-               header( 'Content-type: text/xml' );
-               echo wfAllMessagesMakeXml( $messages );
-       } else {
-               $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | ' .
-                       'HTML |  <a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' );
-               $wgOut->addWikiText( $navText );
-               $wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) );
-       }
-       wfProfileOut( __METHOD__ . '-output' );
-
-       wfProfileOut( __METHOD__ );
-}
-
-function wfAllMessagesMakeXml( $messages ) {
-       global $wgLang;
-       $lang = $wgLang->getCode();
-       $txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
-       $txt .= "<messages lang=\"$lang\">\n";
-       foreach( $messages as $key => $m ) {
-               $txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n";
-       }
-       $txt .= "</messages>";
-       return $txt;
-}
-
-/**
- * Create the messages array, formatted in PHP to copy to language files.
- * @param $messages Messages array.
- * @return The PHP messages array.
- * @todo Make suitable for language files.
- */
-function wfAllMessagesMakePhp( $messages ) {
-       global $wgLang;
-       $txt = "\n\n\$messages = array(\n";
-       foreach( $messages as $key => $m ) {
-               if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) {
-                       continue;
-               } else if ( wfEmptyMsg( $key, $m['msg'] ) ) {
-                       $m['msg'] = '';
-                       $comment = ' #empty';
-               } else {
-                       $comment = '';
-               }
-               $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
-       }
-       $txt .= ');';
-       return $txt;
-}
-
-/**
- * Create a list of messages, formatted in HTML as a list of messages and values and showing differences between the default language file message and the message in MediaWiki: namespace.
- * @param $messages Messages array.
- * @return The HTML list of messages.
- */
-function wfAllMessagesMakeHTMLText( $messages ) {
-       global $wgLang, $wgContLang, $wgUser;
-       wfProfileIn( __METHOD__ );
-
-       $sk = $wgUser->getSkin();
-       $talk = wfMsg( 'talkpagelinktext' );
-
-       $input = Xml::element( 'input', array(
-               'type'    => 'text',
-               'id'      => 'allmessagesinput',
-               'onkeyup' => 'allmessagesfilter()'
-       ), '' );
-       $checkbox = Xml::element( 'input', array(
-               'type'    => 'button',
-               'value'   => wfMsgHtml( 'allmessagesmodified' ),
-               'id'      => 'allmessagescheckbox',
-               'onclick' => 'allmessagesmodified()'
-       ), '' );
-
-       $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . " {$input}{$checkbox} " . '</span>';
-
-       $txt .= '
-<table border="1" cellspacing="0" width="100%" id="allmessagestable">
-       <tr>
-               <th rowspan="2">' . wfMsgHtml( 'allmessagesname' ) . '</th>
-               <th>' . wfMsgHtml( 'allmessagesdefault' ) . '</th>
-       </tr>
-       <tr>
-               <th>' . wfMsgHtml( 'allmessagescurrent' ) . '</th>
-       </tr>';
-
-       wfProfileIn( __METHOD__ . "-check" );
-
-       # This is a nasty hack to avoid doing independent existence checks
-       # without sending the links and table through the slow wiki parser.
-       $pageExists = array(
-               NS_MEDIAWIKI => array(),
-               NS_MEDIAWIKI_TALK => array()
-       );
-       $dbr = wfGetDB( DB_SLAVE );
-       $page = $dbr->tableName( 'page' );
-       $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")";
-       $res = $dbr->query( $sql );
-       while( $s = $dbr->fetchObject( $res ) ) {
-               $pageExists[$s->page_namespace][$s->page_title] = true;
-       }
-       $dbr->freeResult( $res );
-       wfProfileOut( __METHOD__ . "-check" );
-
-       wfProfileIn( __METHOD__ . "-output" );
-
-       $i = 0;
-
-       foreach( $messages as $key => $m ) {
-               $title = $wgLang->ucfirst( $key );
-               if( $wgLang->getCode() != $wgContLang->getCode() ) {
-                       $title .= '/' . $wgLang->getCode();
-               }
-
-               $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title );
-               $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
-
-               $changed = ( $m['statmsg'] != $m['msg'] );
-               $message = htmlspecialchars( $m['statmsg'] );
-               $mw = htmlspecialchars( $m['msg'] );
-
-               if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) {
-                       $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .  htmlspecialchars( $key ) . '</span>' );
-               } else {
-                       $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .  htmlspecialchars( $key ) . '</span>' );
-               }
-               if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) {
-                       $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
-               } else {
-                       $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
-               }
-
-               $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) );
-               $anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>";
-
-               if( $changed ) {
-                       $txt .= "
-       <tr class=\"orig\" id=\"sp-allmessages-r1-$i\">
-               <td rowspan=\"2\">
-                       $anchor$pageLink<br />$talkLink
-               </td><td>
-$message
-               </td>
-       </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\">
-               <td>
-$mw
-               </td>
-       </tr>";
-               } else {
-                       $txt .= "
-       <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
-               <td>
-                       $anchor$pageLink<br />$talkLink
-               </td><td>
-$mw
-               </td>
-       </tr>";
-               }
-               $i++;
-       }
-       $txt .= '</table>';
-       wfProfileOut( __METHOD__ . '-output' );
-
-       wfProfileOut( __METHOD__ );
-       return $txt;
-}
diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php
deleted file mode 100644 (file)
index 7223e31..0000000
+++ /dev/null
@@ -1,404 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point : initialise variables and call subfunctions.
- * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
- * @param $specialPage See the SpecialPage object.
- */
-function wfSpecialAllpages( $par=NULL, $specialPage ) {
-       global $wgRequest, $wgOut, $wgContLang;
-
-       # GET values
-       $from = $wgRequest->getVal( 'from' );
-       $namespace = $wgRequest->getInt( 'namespace' );
-
-       $namespaces = $wgContLang->getNamespaces();
-
-       $indexPage = new SpecialAllpages();
-
-       $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) )  ?
-               wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
-               wfMsg( 'allarticles' )
-               );
-
-       if ( isset($par) ) {
-               $indexPage->showChunk( $namespace, $par, $specialPage->including() );
-       } elseif ( isset($from) ) {
-               $indexPage->showChunk( $namespace, $from, $specialPage->including() );
-       } else {
-               $indexPage->showToplevel ( $namespace, $specialPage->including() );
-       }
-}
-
-/**
- * Implements Special:Allpages
- * @ingroup SpecialPage
- */
-class SpecialAllpages {
-       /**
-        * Maximum number of pages to show on single subpage.
-        */
-       protected $maxPerPage = 960;
-
-       /**
-        * Name of this special page. Used to make title objects that reference back
-        * to this page.
-        */
-       protected $name = 'Allpages';
-
-       /**
-        * Determines, which message describes the input field 'nsfrom'.
-        */
-       protected $nsfromMsg = 'allpagesfrom';
-
-/**
- * HTML for the top form
- * @param integer $namespace A namespace constant (default NS_MAIN).
- * @param string $from Article name we are starting listing at.
- */
-function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
-       global $wgScript;
-       $t = SpecialPage::getTitleFor( $this->name );
-
-       $out  = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
-       $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
-       $out .= Xml::hidden( 'title', $t->getPrefixedText() );
-       $out .= Xml::openElement( 'fieldset' );
-       $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
-       $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
-       $out .= "<tr>
-                       <td class='mw-label'>" .
-                               Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) .
-                       "</td>
-                       <td class='mw-input'>" .
-                               Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) .
-                       "</td>
-               </tr>
-               <tr>
-                       <td class='mw-label'>" .
-                               Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
-                       "</td>
-                       <td class='mw-input'>" .
-                               Xml::namespaceSelector( $namespace, null ) . ' ' .
-                               Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
-                       "</td>
-                       </tr>";
-       $out .= Xml::closeElement( 'table' );
-       $out .= Xml::closeElement( 'fieldset' );
-       $out .= Xml::closeElement( 'form' );
-       $out .= Xml::closeElement( 'div' );
-       return $out;
-}
-
-/**
- * @param integer $namespace (default NS_MAIN)
- */
-function showToplevel ( $namespace = NS_MAIN, $including = false ) {
-       global $wgOut, $wgContLang;
-       $align = $wgContLang->isRtl() ? 'left' : 'right';
-
-       # TODO: Either make this *much* faster or cache the title index points
-       # in the querycache table.
-
-       $dbr = wfGetDB( DB_SLAVE );
-       $out = "";
-       $where = array( 'page_namespace' => $namespace );
-
-       global $wgMemc;
-       $key = wfMemcKey( 'allpages', 'ns', $namespace );
-       $lines = $wgMemc->get( $key );
-
-       if( !is_array( $lines ) ) {
-               $options = array( 'LIMIT' => 1 );
-               if ( ! $dbr->implicitOrderby() ) {
-                       $options['ORDER BY'] = 'page_title';
-               }
-               $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
-               $lastTitle = $firstTitle;
-
-               # This array is going to hold the page_titles in order.
-               $lines = array( $firstTitle );
-
-               # If we are going to show n rows, we need n+1 queries to find the relevant titles.
-               $done = false;
-               for( $i = 0; !$done; ++$i ) {
-                       // Fetch the last title of this chunk and the first of the next
-                       $chunk = is_null( $lastTitle )
-                               ? ''
-                               : 'page_title >= ' . $dbr->addQuotes( $lastTitle );
-                       $res = $dbr->select(
-                               'page', /* FROM */
-                               'page_title', /* WHAT */
-                               $where + array($chunk),
-                               __METHOD__,
-                               array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') );
-
-                       if ( $s = $dbr->fetchObject( $res ) ) {
-                               array_push( $lines, $s->page_title );
-                       } else {
-                               // Final chunk, but ended prematurely. Go back and find the end.
-                               $endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
-                                       array(
-                                               'page_namespace' => $namespace,
-                                               $chunk
-                                       ), __METHOD__ );
-                               array_push( $lines, $endTitle );
-                               $done = true;
-                       }
-                       if( $s = $dbr->fetchObject( $res ) ) {
-                               array_push( $lines, $s->page_title );
-                               $lastTitle = $s->page_title;
-                       } else {
-                               // This was a final chunk and ended exactly at the limit.
-                               // Rare but convenient!
-                               $done = true;
-                       }
-                       $dbr->freeResult( $res );
-               }
-               $wgMemc->add( $key, $lines, 3600 );
-       }
-
-       // If there are only two or less sections, don't even display them.
-       // Instead, display the first section directly.
-       if( count( $lines ) <= 2 ) {
-               $this->showChunk( $namespace, '', $including );
-               return;
-       }
-
-       # At this point, $lines should contain an even number of elements.
-       $out .= "<table class='allpageslist' style='background: inherit;'>";
-       while ( count ( $lines ) > 0 ) {
-               $inpoint = array_shift ( $lines );
-               $outpoint = array_shift ( $lines );
-               $out .= $this->showline ( $inpoint, $outpoint, $namespace, false );
-       }
-       $out .= '</table>';
-       $nsForm = $this->namespaceForm( $namespace, '', false );
-
-       # Is there more?
-       if ( $including ) {
-               $out2 = '';
-       } else {
-               $morelinks = '';
-               if ( $morelinks != '' ) {
-                       $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
-                       $out2 .= '<tr valign="top"><td>' . $nsForm;
-                       $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">';
-                       $out2 .= $morelinks . '</td></tr></table><hr />';
-               } else {
-                       $out2 = $nsForm . '<hr />';
-               }
-       }
-
-       $wgOut->addHtml( $out2 . $out );
-}
-
-/**
- * @todo Document
- * @param string $from
- * @param integer $namespace (Default NS_MAIN)
- */
-function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
-       global $wgContLang;
-       $align = $wgContLang->isRtl() ? 'left' : 'right';
-       $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
-       $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
-       $queryparams = ($namespace ? "namespace=$namespace" : '');
-       $special = SpecialPage::getTitleFor( $this->name, $inpoint );
-       $link = $special->escapeLocalUrl( $queryparams );
-
-       $out = wfMsgHtml(
-               'alphaindexline',
-               "<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">",
-               "</a></td><td><a href=\"$link\">$outpointf</a>"
-       );
-       return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
-}
-
-/**
- * @param integer $namespace (Default NS_MAIN)
- * @param string $from list all pages from this name (default FALSE)
- */
-function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
-       global $wgOut, $wgUser, $wgContLang;
-
-       $sk = $wgUser->getSkin();
-
-       $fromList = $this->getNamespaceKeyAndText($namespace, $from);
-       $namespaces = $wgContLang->getNamespaces();
-       $align = $wgContLang->isRtl() ? 'left' : 'right';
-
-       $n = 0;
-
-       if ( !$fromList ) {
-               $out = wfMsgWikiHtml( 'allpagesbadtitle' );
-       } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
-               // Show errormessage and reset to NS_MAIN
-               $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
-               $namespace = NS_MAIN;
-       } else {
-               list( $namespace, $fromKey, $from ) = $fromList;
-
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select( 'page',
-                       array( 'page_namespace', 'page_title', 'page_is_redirect' ),
-                       array(
-                               'page_namespace' => $namespace,
-                               'page_title >= ' . $dbr->addQuotes( $fromKey )
-                       ),
-                       __METHOD__,
-                       array(
-                               'ORDER BY'  => 'page_title',
-                               'LIMIT'     => $this->maxPerPage + 1,
-                               'USE INDEX' => 'name_title',
-                       )
-               );
-
-               if( $res->numRows() > 0 ) {
-                       $out = '<table style="background: inherit;" border="0" width="100%">';
-       
-                       while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
-                               $t = Title::makeTitle( $s->page_namespace, $s->page_title );
-                               if( $t ) {
-                                       $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
-                                               $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
-                                               ($s->page_is_redirect ? '</div>' : '' );
-                               } else {
-                                       $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
-                               }
-                               if( $n % 3 == 0 ) {
-                                       $out .= '<tr>';
-                               }
-                               $out .= "<td width=\"33%\">$link</td>";
-                               $n++;
-                               if( $n % 3 == 0 ) {
-                                       $out .= '</tr>';
-                               }
-                       }
-                       if( ($n % 3) != 0 ) {
-                               $out .= '</tr>';
-                       }
-                       $out .= '</table>';
-               } else {
-                       $out = '';
-               }
-       }
-
-       if ( $including ) {
-               $out2 = '';
-       } else {
-               if( $from == '' ) {
-                       // First chunk; no previous link.
-                       $prevTitle = null;
-               } else {
-                       # Get the last title from previous chunk
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $res_prev = $dbr->select(
-                               'page',
-                               'page_title',
-                               array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
-                               __METHOD__,
-                               array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) )
-                       );
-
-                       # Get first title of previous complete chunk
-                       if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
-                               $pt = $dbr->fetchObject( $res_prev );
-                               $prevTitle = Title::makeTitle( $namespace, $pt->page_title );
-                       } else {
-                               # The previous chunk is not complete, need to link to the very first title
-                               # available in the database
-                               $options = array( 'LIMIT' => 1 );
-                               if ( ! $dbr->implicitOrderby() ) {
-                                       $options['ORDER BY'] = 'page_title';
-                               }
-                               $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), __METHOD__, $options );
-                               # Show the previous link if it s not the current requested chunk
-                               if( $from != $reallyFirstPage_title ) {
-                                       $prevTitle =  Title::makeTitle( $namespace, $reallyFirstPage_title );
-                               } else {
-                                       $prevTitle = null;
-                               }
-                       }
-               }
-
-               $nsForm = $this->namespaceForm( $namespace, $from );
-               $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
-               $out2 .= '<tr valign="top"><td>' . $nsForm;
-               $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
-                               $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
-                                       wfMsgHtml ( 'allpages' ) );
-
-               $self = SpecialPage::getTitleFor( 'Allpages' );
-
-               # Do we put a previous link ?
-               if( isset( $prevTitle ) &&  $pt = $prevTitle->getText() ) {
-                       $q = 'from=' . $prevTitle->getPartialUrl()
-                               . ( $namespace ? '&namespace=' . $namespace : '' );
-                       $prevLink = $sk->makeKnownLinkObj( $self,
-                               wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q );
-                       $out2 .= ' | ' . $prevLink;
-               }
-
-               if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) {
-                       # $s is the first link of the next chunk
-                       $t = Title::MakeTitle($namespace, $s->page_title);
-                       $q = 'from=' . $t->getPartialUrl()
-                               . ( $namespace ? '&namespace=' . $namespace : '' );
-                       $nextLink = $sk->makeKnownLinkObj( $self,
-                               wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q );
-                       $out2 .= ' | ' . $nextLink;
-               }
-               $out2 .= "</td></tr></table><hr />";
-       }
-
-       $wgOut->addHtml( $out2 . $out );
-       if( isset($prevLink) or isset($nextLink) ) {
-               $wgOut->addHtml( '<hr /><p style="font-size: smaller; float: ' . $align . '">' );
-               if( isset( $prevLink ) ) {
-                       $wgOut->addHTML( $prevLink );
-               }
-               if( isset( $prevLink ) && isset( $nextLink ) ) {
-                       $wgOut->addHTML( ' | ' );
-               }
-               if( isset( $nextLink ) ) {
-                       $wgOut->addHTML( $nextLink );
-               }
-               $wgOut->addHTML( '</p>' );
-
-       }
-
-}
-
-/**
- * @param int $ns the namespace of the article
- * @param string $text the name of the article
- * @return array( int namespace, string dbkey, string pagename ) or NULL on error
- * @static (sort of)
- * @access private
- */
-function getNamespaceKeyAndText ($ns, $text) {
-       if ( $text == '' )
-               return array( $ns, '', '' ); # shortcut for common case
-
-       $t = Title::makeTitleSafe($ns, $text);
-       if ( $t && $t->isLocal() ) {
-               return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
-       } else if ( $t ) {
-               return NULL;
-       }
-
-       # try again, in case the problem was an empty pagename
-       $text = preg_replace('/(#|$)/', 'X$1', $text);
-       $t = Title::makeTitleSafe($ns, $text);
-       if ( $t && $t->isLocal() ) {
-               return array( $t->getNamespace(), '', '' );
-       } else {
-               return NULL;
-       }
-}
-}
diff --git a/includes/SpecialAncientpages.php b/includes/SpecialAncientpages.php
deleted file mode 100644 (file)
index 724d34b..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Implements Special:Ancientpages
- * @ingroup SpecialPage
- */
-class AncientPagesPage extends QueryPage {
-
-       function getName() {
-               return "Ancientpages";
-       }
-
-       function isExpensive() {
-               return true;
-       }
-
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               global $wgDBtype;
-               $db = wfGetDB( DB_SLAVE );
-               $page = $db->tableName( 'page' );
-               $revision = $db->tableName( 'revision' );
-               #$use_index = $db->useIndexClause( 'cur_timestamp' ); # FIXME! this is gone
-               $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' :
-                       'EXTRACT(epoch FROM rev_timestamp)';
-               return
-                       "SELECT 'Ancientpages' as type,
-                                       page_namespace as namespace,
-                               page_title as title,
-                               $epoch as value
-                       FROM $page, $revision
-                       WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0
-                         AND page_latest=rev_id";
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-
-               $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $result->value ), true );
-               $title = Title::makeTitle( $result->namespace, $result->title );
-               $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
-               return wfSpecialList($link, $d);
-       }
-}
-
-function wfSpecialAncientpages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $app = new AncientPagesPage();
-
-       $app->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php
deleted file mode 100644 (file)
index 5ea25ca..0000000
+++ /dev/null
@@ -1,494 +0,0 @@
-<?php
-/**
- * Constructor for Special:Blockip page
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor
- */
-function wfSpecialBlockip( $par ) {
-       global $wgUser, $wgOut, $wgRequest;
-
-       # Can't block when the database is locked
-       if( wfReadOnly() ) {
-               $wgOut->readOnlyPage();
-               return;
-       }
-
-       # Permission check
-       if( !$wgUser->isAllowed( 'block' ) ) {
-               $wgOut->permissionRequired( 'block' );
-               return;
-       }
-
-       $ipb = new IPBlockForm( $par );
-
-       $action = $wgRequest->getVal( 'action' );
-       if ( 'success' == $action ) {
-               $ipb->showSuccess();
-       } else if ( $wgRequest->wasPosted() && 'submit' == $action &&
-               $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
-               $ipb->doSubmit();
-       } else {
-               $ipb->showForm( '' );
-       }
-}
-
-/**
- * Form object for the Special:Blockip page.
- *
- * @ingroup SpecialPage
- */
-class IPBlockForm {
-       var $BlockAddress, $BlockExpiry, $BlockReason;
-#      var $BlockEmail;
-
-       function IPBlockForm( $par ) {
-               global $wgRequest, $wgUser;
-
-               $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) );
-               $this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' );
-               $this->BlockReason = $wgRequest->getText( 'wpBlockReason' );
-               $this->BlockReasonList = $wgRequest->getText( 'wpBlockReasonList' );
-               $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') );
-               $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' );
-
-               # Unchecked checkboxes are not included in the form data at all, so having one
-               # that is true by default is a bit tricky
-               $byDefault = !$wgRequest->wasPosted();
-               $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault );
-               $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault );
-               $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault );
-               $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false );
-               $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false );
-               # Re-check user's rights to hide names, very serious, defaults to 0
-               $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0;
-       }
-
-       function showForm( $err ) {
-               global $wgOut, $wgUser, $wgSysopUserBans;
-
-               $wgOut->setPagetitle( wfMsg( 'blockip' ) );
-               $wgOut->addWikiMsg( 'blockiptext' );
-
-               if($wgSysopUserBans) {
-                       $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' );
-               } else {
-                       $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' );
-               }
-               $mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' );
-               $mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' );
-               $mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' );
-               $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' );
-
-               $titleObj = SpecialPage::getTitleFor( 'Blockip' );
-
-               if ( "" != $err ) {
-                       $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
-                       $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $err ) );
-               }
-
-               $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' );
-
-               $showblockoptions = $scBlockExpiryOptions != '-';
-               if (!$showblockoptions)
-                       $mIpbother = $mIpbexpiry;
-
-               $blockExpiryFormOptions = Xml::option( wfMsg( 'ipbotheroption' ), 'other' );
-               foreach (explode(',', $scBlockExpiryOptions) as $option) {
-                       if ( strpos($option, ":") === false ) $option = "$option:$option";
-                       list($show, $value) = explode(":", $option);
-                       $show = htmlspecialchars($show);
-                       $value = htmlspecialchars($value);
-                       $blockExpiryFormOptions .= Xml::option( $show, $value, $this->BlockExpiry === $value ? true : false ) . "\n";
-               }
-
-               $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList',
-                       wfMsgForContent( 'ipbreason-dropdown' ),
-                       wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 );
-
-               global $wgStylePath, $wgStyleVersion;
-               $wgOut->addHTML(
-                       Xml::tags( 'script', array( 'type' => 'text/javascript', 'src' => "$wgStylePath/common/block.js?$wgStyleVersion" ), '' ) .
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( "action=submit" ), 'id' => 'blockip' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'blockip-legend' ) ) .
-                       Xml::openElement( 'table', array ( 'border' => '0', 'id' => 'mw-blockip-table' ) ) .
-                       "<tr>
-                               <td class='mw-label'>
-                                       {$mIpaddress}
-                               </td>
-                               <td class='mw-input'>" .
-                                       Xml::input( 'wpBlockAddress', 45, $this->BlockAddress,
-                                               array(
-                                                       'tabindex' => '1',
-                                                       'id' => 'mw-bi-target',
-                                                       'onchange' => 'updateBlockOptions()' ) ). "
-                               </td>
-                       </tr>
-                       <tr>"
-               );
-               if ( $showblockoptions ) {
-                       $wgOut->addHTML("
-                               <td class='mw-label'>
-                                       {$mIpbexpiry}
-                               </td>
-                               <td class='mw-input'>" .
-                                       Xml::tags( 'select',
-                                               array(
-                                                       'id' => 'wpBlockExpiry',
-                                                       'name' => 'wpBlockExpiry',
-                                                       'onchange' => 'considerChangingExpiryFocus()',
-                                                       'tabindex' => '2' ),
-                                               $blockExpiryFormOptions ) .
-                               "</td>"
-                       );
-               }
-               $wgOut->addHTML("
-                       </tr>
-                       <tr id='wpBlockOther'>
-                               <td class='mw-label'>
-                                       {$mIpbother}
-                               </td>
-                               <td class='mw-input'>" .
-                                       Xml::input( 'wpBlockOther', 45, $this->BlockOther,
-                                               array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class='mw-label'>
-                                       {$mIpbreasonother}
-                               </td>
-                               <td class='mw-input'>
-                                       {$reasonDropDown}
-                               </td>
-                       </tr>
-                       <tr id=\"wpBlockReason\">
-                               <td class='mw-label'>
-                                       {$mIpbreason}
-                               </td>
-                               <td class='mw-input'>" .
-                                       Xml::input( 'wpBlockReason', 45, $this->BlockReason,
-                                               array( 'tabindex' => '5', 'id' => 'mw-bi-reason', 'maxlength'=> '200' ) ) . "
-                               </td>
-                       </tr>
-                       <tr id='wpAnonOnlyRow'>
-                               <td>&nbsp;</td>
-                               <td class='mw-input'>" .
-                               Xml::checkLabel( wfMsg( 'ipbanononly' ),
-                                               'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly,
-                                               array( 'tabindex' => '6' ) ) . "
-                               </td>
-                       </tr>
-                       <tr id='wpCreateAccountRow'>
-                               <td>&nbsp;</td>
-                               <td class='mw-input'>" .
-                                       Xml::checkLabel( wfMsg( 'ipbcreateaccount' ),
-                                               'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount,
-                                               array( 'tabindex' => '7' ) ) . "
-                               </td>
-                       </tr>
-                       <tr id='wpEnableAutoblockRow'>
-                               <td>&nbsp;</td>
-                               <td class='mw-input'>" .
-                                       Xml::checkLabel( wfMsg( 'ipbenableautoblock' ),
-                                               'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock,
-                                               array( 'tabindex' => '8' ) ) . "
-                               </td>
-                       </tr>"
-               );
-
-               global $wgSysopEmailBans;
-               if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
-                       $wgOut->addHTML("
-                               <tr id='wpEnableEmailBan'>
-                                       <td>&nbsp;</td>
-                                       <td class='mw-input'>" .
-                                               Xml::checkLabel( wfMsg( 'ipbemailban' ),
-                                                       'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
-                                                       array( 'tabindex' => '9' )) . "
-                                       </td>
-                               </tr>"
-                       );
-               }
-
-               // Allow some users to hide name from block log, blocklist and listusers
-               if ( $wgUser->isAllowed( 'hideuser' ) ) {
-                       $wgOut->addHTML("
-                               <tr id='wpEnableHideUser'>
-                                       <td>&nbsp;</td>
-                                       <td class='mw-input'>" .
-                                               Xml::checkLabel( wfMsg( 'ipbhidename' ),
-                                                       'wpHideName', 'wpHideName', $this->BlockHideName,
-                                                       array( 'tabindex' => '10' ) ) . "
-                                       </td>
-                               </tr>"
-                       );
-               }
-               
-               # Watchlist their user page?
-               $wgOut->addHTML("
-                       <tr id='wpEnableWatchUser'>
-                               <td>&nbsp;</td>
-                               <td class='mw-input'>" .
-                                       Xml::checkLabel( wfMsg( 'ipbwatchuser' ),
-                                               'wpWatchUser', 'wpWatchUser', $this->BlockWatchUser,
-                                               array( 'tabindex' => '11' ) ) . "
-                               </td>
-                       </tr>"
-               );
-
-               $wgOut->addHTML("
-                       <tr>
-                               <td style='padding-top: 1em'>&nbsp;</td>
-                               <td  class='mw-submit' style='padding-top: 1em'>" .
-                                       Xml::submitButton( wfMsg( 'ipbsubmit' ),
-                                               array( 'name' => 'wpBlock', 'tabindex' => '12' ) ) . "
-                               </td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) .
-                       Xml::tags( 'script', array( 'type' => 'text/javascript' ), 'updateBlockOptions()' ) . "\n"
-               );
-
-               $wgOut->addHtml( $this->getConvenienceLinks() );
-
-               $user = User::newFromName( $this->BlockAddress );
-               if( is_object( $user ) ) {
-                       $this->showLogFragment( $wgOut, $user->getUserPage() );
-               } elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) {
-                       $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
-               } elseif( preg_match( '/^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}/', $this->BlockAddress ) ) {
-                       $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
-               }
-       }
-
-       /**
-        * Backend block code.
-        * $userID and $expiry will be filled accordingly
-        * @return array(message key, arguments) on failure, empty array on success
-        */
-       function doBlock(&$userId = null, &$expiry = null)
-       {
-               global $wgUser, $wgSysopUserBans, $wgSysopRangeBans;
-
-               $userId = 0;
-               # Expand valid IPv6 addresses, usernames are left as is
-               $this->BlockAddress = IP::sanitizeIP( $this->BlockAddress );
-               # isIPv4() and IPv6() are used for final validation
-               $rxIP4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
-               $rxIP6 = '\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}';
-               $rxIP = "($rxIP4|$rxIP6)";
-
-               # Check for invalid specifications
-               if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
-                       $matches = array();
-                       if ( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) {
-                               # IPv4
-                               if ( $wgSysopRangeBans ) {
-                                       if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) {
-                                               return array('ip_range_invalid');
-                                       }
-                                       $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
-                               } else {
-                                       # Range block illegal
-                                       return array('range_block_disabled');
-                               }
-                       } else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) {
-                               # IPv6
-                               if ( $wgSysopRangeBans ) {
-                                       if ( !IP::isIPv6( $this->BlockAddress ) || $matches[2] < 64 || $matches[2] > 128 ) {
-                                               return array('ip_range_invalid');
-                                       }
-                                       $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
-                               } else {
-                                       # Range block illegal
-                                       return array('range_block_disabled');
-                               }
-                       } else {
-                               # Username block
-                               if ( $wgSysopUserBans ) {
-                                       $user = User::newFromName( $this->BlockAddress );
-                                       if( !is_null( $user ) && $user->getId() ) {
-                                               # Use canonical name
-                                               $userId = $user->getId();
-                                               $this->BlockAddress = $user->getName();
-                                       } else {
-                                               return array('nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) );
-                                       }
-                               } else {
-                                       return array('badipaddress');
-                               }
-                       }
-               }
-
-               $reasonstr = $this->BlockReasonList;
-               if ( $reasonstr != 'other' && $this->BlockReason != '') {
-                       // Entry from drop down menu + additional comment
-                       $reasonstr .= ': ' . $this->BlockReason;
-               } elseif ( $reasonstr == 'other' ) {
-                       $reasonstr = $this->BlockReason;
-               }
-
-               $expirestr = $this->BlockExpiry;
-               if( $expirestr == 'other' )
-                       $expirestr = $this->BlockOther;
-
-               if (strlen($expirestr) == 0) {
-                       return array('ipb_expiry_invalid');
-               }
-               
-               if ( false === ($expiry = Block::parseExpiryInput( $expirestr )) ) {
-                       // Bad expiry.
-                       return array('ipb_expiry_invalid');
-               }
-               
-               if( $this->BlockHideName && $expiry != 'infinity' ) {
-                       // Bad expiry.
-                       return array('ipb_expiry_temp');
-               }
-
-               # Create block
-               # Note: for a user block, ipb_address is only for display purposes
-               $block = new Block( $this->BlockAddress, $userId, $wgUser->getId(),
-                       $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly,
-                       $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName,
-                       $this->BlockEmail );
-
-               if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) {
-
-                       if ( !$block->insert() ) {
-                               return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress));
-                       }
-
-                       wfRunHooks('BlockIpComplete', array($block, $wgUser));
-
-                       if ( $this->BlockWatchUser ) { 
-                               $wgUser->addWatch ( Title::makeTitle( NS_USER, $this->BlockAddress ) );
-                       }
-
-                       # Prepare log parameters
-                       $logParams = array();
-                       $logParams[] = $expirestr;
-                       $logParams[] = $this->blockLogFlags();
-
-                       # Make log entry, if the name is hidden, put it in the oversight log
-                       $log_type = ($this->BlockHideName) ? 'suppress' : 'block';
-                       $log = new LogPage( $log_type );
-                       $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ),
-                         $reasonstr, $logParams );
-
-                       # Report to the user
-                       return array();
-               }
-               else
-                       return array('hookaborted');
-       }
-
-       /**
-        * UI entry point for blocking
-        * Wraps around doBlock()
-        */
-       function doSubmit()
-       {
-               global $wgOut;
-               $retval = $this->doBlock();
-               if(empty($retval)) {
-                       $titleObj = SpecialPage::getTitleFor( 'Blockip' );
-                       $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
-                               urlencode( $this->BlockAddress ) ) );
-                       return;
-               }
-               $key = array_shift($retval);
-               $this->showForm(wfMsgReal($key, $retval));
-       }
-
-       function showSuccess() {
-               global $wgOut;
-
-               $wgOut->setPagetitle( wfMsg( 'blockip' ) );
-               $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
-               $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
-               $wgOut->addHtml( $text );
-       }
-
-       function showLogFragment( $out, $title ) {
-               $out->addHtml( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
-               LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText() );
-       }
-
-       /**
-        * Return a comma-delimited list of "flags" to be passed to the log
-        * reader for this block, to provide more information in the logs
-        *
-        * @return array
-        */
-       private function blockLogFlags() {
-               $flags = array();
-               if( $this->BlockAnonOnly && IP::isIPAddress( $this->BlockAddress ) )
-                                       // when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log
-                       $flags[] = 'anononly';
-               if( $this->BlockCreateAccount )
-                       $flags[] = 'nocreate';
-               if( !$this->BlockEnableAutoblock )
-                       $flags[] = 'noautoblock';
-               if ( $this->BlockEmail )
-                       $flags[] = 'noemail';
-               return implode( ',', $flags );
-       }
-
-       /**
-        * Builds unblock and block list links
-        *
-        * @return string
-        */
-       private function getConvenienceLinks() {
-               global $wgUser;
-               $skin = $wgUser->getSkin();
-               $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
-               $links[] = $this->getUnblockLink( $skin );
-               $links[] = $this->getBlockListLink( $skin );
-               return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>';
-       }
-
-       /**
-        * Build a convenient link to unblock the given username or IP
-        * address, if available; otherwise link to a blank unblock
-        * form
-        *
-        * @param $skin Skin to use
-        * @return string
-        */
-       private function getUnblockLink( $skin ) {
-               $list = SpecialPage::getTitleFor( 'Ipblocklist' );
-               if( $this->BlockAddress ) {
-                       $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
-                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ),
-                               'action=unblock&ip=' . urlencode( $this->BlockAddress ) );
-               } else {
-                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ),      'action=unblock' );
-               }
-       }
-
-       /**
-        * Build a convenience link to the block list
-        *
-        * @param $skin Skin to use
-        * @return string
-        */
-       private function getBlockListLink( $skin ) {
-               $list = SpecialPage::getTitleFor( 'Ipblocklist' );
-               if( $this->BlockAddress ) {
-                       $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
-                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ),
-                               'ip=' . urlencode( $this->BlockAddress ) );
-               } else {
-                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) );
-               }
-       }
-}
diff --git a/includes/SpecialBlockme.php b/includes/SpecialBlockme.php
deleted file mode 100644 (file)
index f222e3c..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-function wfSpecialBlockme() {
-       global $wgRequest, $wgBlockOpenProxies, $wgOut, $wgProxyKey;
-
-       $ip = wfGetIP();
-
-       if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
-               $wgOut->addWikiMsg( 'proxyblocker-disabled' );
-               return;
-       }
-
-       $blockerName = wfMsg( "proxyblocker" );
-       $reason = wfMsg( "proxyblockreason" );
-
-       $u = User::newFromName( $blockerName );
-       $id = $u->idForName();
-       if ( !$id ) {
-               $u = User::newFromName( $blockerName );
-               $u->addToDatabase();
-               $u->setPassword( bin2hex( mt_rand(0, 0x7fffffff ) ) );
-               $u->saveSettings();
-               $id = $u->getID();
-       }
-
-       $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() );
-       $block->insert();
-
-       $wgOut->addWikiMsg( "proxyblocksuccess" );
-}
diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php
deleted file mode 100644 (file)
index 0690c5c..0000000
+++ /dev/null
@@ -1,110 +0,0 @@
-<?php
-
-/**
- * Special page outputs information on sourcing a book with a particular ISBN
- * The parser creates links to this page when dealing with ISBNs in wikitext
- *
- * @author Rob Church <robchur@gmail.com>
- * @todo Validate ISBNs using the standard check-digit method
- * @ingroup SpecialPages
- */
-class SpecialBookSources extends SpecialPage {
-
-       /**
-        * ISBN passed to the page, if any
-        */
-       private $isbn = '';
-
-       /**
-        * Constructor
-        */
-       public function __construct() {
-               parent::__construct( 'Booksources' );
-       }
-
-       /**
-        * Show the special page
-        *
-        * @param $isbn ISBN passed as a subpage parameter
-        */
-       public function execute( $isbn ) {
-               global $wgOut, $wgRequest;
-               $this->setHeaders();
-               $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
-               $wgOut->addWikiMsg( 'booksources-summary' );
-               $wgOut->addHtml( $this->makeForm() );
-               if( strlen( $this->isbn ) > 0 )
-                       $this->showList();
-       }
-
-       /**
-        * Trim ISBN and remove characters which aren't required
-        *
-        * @param $isbn Unclean ISBN
-        * @return string
-        */
-       private function cleanIsbn( $isbn ) {
-               return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
-       }
-
-       /**
-        * Generate a form to allow users to enter an ISBN
-        *
-        * @return string
-        */
-       private function makeForm() {
-               global $wgScript;
-               $title = self::getTitleFor( 'Booksources' );
-               $form  = '<fieldset><legend>' . wfMsgHtml( 'booksources-search-legend' ) . '</legend>';
-               $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
-               $form .= Xml::hidden( 'title', $title->getPrefixedText() );
-               $form .= '<p>' . Xml::inputLabel( wfMsg( 'booksources-isbn' ), 'isbn', 'isbn', 20, $this->isbn );
-               $form .= '&nbsp;' . Xml::submitButton( wfMsg( 'booksources-go' ) ) . '</p>';
-               $form .= Xml::closeElement( 'form' );
-               $form .= '</fieldset>';
-               return $form;
-       }
-
-       /**
-        * Determine where to get the list of book sources from,
-        * format and output them
-        *
-        * @return string
-        */
-       private function showList() {
-               global $wgOut, $wgContLang;
-
-               # Hook to allow extensions to insert additional HTML,
-               # e.g. for API-interacting plugins and so on
-               wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) );
-
-               # Check for a local page such as Project:Book_sources and use that if available
-               $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language
-               if( is_object( $title ) && $title->exists() ) {
-                       $rev = Revision::newFromTitle( $title );
-                       $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
-                       return true;
-               }
-
-               # Fall back to the defaults given in the language file
-               $wgOut->addWikiMsg( 'booksources-text' );
-               $wgOut->addHtml( '<ul>' );
-               $items = $wgContLang->getBookstoreList();
-               foreach( $items as $label => $url )
-                       $wgOut->addHtml( $this->makeListItem( $label, $url ) );
-               $wgOut->addHtml( '</ul>' );
-               return true;
-       }
-
-       /**
-        * Format a book source list item
-        *
-        * @param $label Book source label
-        * @param $url Book source URL
-        * @return string
-        */
-       private function makeListItem( $label, $url ) {
-               $url = str_replace( '$1', $this->isbn, $url );
-               return '<li><a href="' . htmlspecialchars( $url ) . '">' . htmlspecialchars( $label ) . '</a></li>';
-       }
-}
diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php
deleted file mode 100644 (file)
index 0a16e6d..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page listing redirects to non existent page. Those should be
- * fixed to point to an existing page.
- * @ingroup SpecialPage
- */
-class BrokenRedirectsPage extends PageQueryPage {
-       var $targets = array();
-
-       function getName() {
-               return 'BrokenRedirects';
-       }
-
-       function isExpensive( ) { return true; }
-       function isSyndicated() { return false; }
-
-       function getPageHeader( ) {
-               return wfMsgExt( 'brokenredirectstext', array( 'parse' ) );
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
-
-               $sql = "SELECT 'BrokenRedirects'  AS type,
-                               p1.page_namespace AS namespace,
-                               p1.page_title     AS title,
-                               rd_namespace,
-                               rd_title
-                          FROM $redirect AS rd
-                   JOIN $page p1 ON (rd.rd_from=p1.page_id)
-                     LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title )
-                                 WHERE rd_namespace >= 0
-                                   AND p2.page_namespace IS NULL";
-               return $sql;
-       }
-
-       function getOrder() {
-               return '';
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgUser, $wgContLang;
-
-               $fromObj = Title::makeTitle( $result->namespace, $result->title );
-               if ( isset( $result->rd_title ) ) {
-                       $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title );
-               } else {
-                       $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links
-                       if ( $blinks ) {
-                               $toObj = $blinks[0];
-                       } else {
-                               $toObj = false;
-                       }
-               }
-
-               // $toObj may very easily be false if the $result list is cached
-               if ( !is_object( $toObj ) ) {
-                       return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>';
-               }
-
-               $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' );
-               $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' );
-               $to   = $skin->makeBrokenLinkObj( $toObj );
-               $arr = $wgContLang->getArrow();
-
-               $out = "{$from} {$edit}";
-
-               if( $wgUser->isAllowed( 'delete' ) ) {
-                       $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' );
-                       $out .= " {$delete}";
-               }
-
-               $out .= " {$arr} {$to}";
-               return $out;
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialBrokenRedirects() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $sbr = new BrokenRedirectsPage();
-
-       return $sbr->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php
deleted file mode 100644 (file)
index 951c222..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-function wfSpecialCategories( $par=null ) {
-       global $wgOut, $wgRequest;
-
-       if( $par == '' ) {
-               $from = $wgRequest->getText( 'from' );
-       } else {
-               $from = $par;
-       }
-       $cap = new CategoryPager( $from );
-       $wgOut->addHTML(
-               wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
-               $cap->getStartForm( $from ) .
-               $cap->getNavigationBar() .
-               '<ul>' . $cap->getBody() . '</ul>' .
-               $cap->getNavigationBar()
-       );
-}
-
-/**
- * TODO: Allow sorting by count.  We need to have a unique index to do this
- * properly.
- *
- * @ingroup SpecialPage Pager
- */
-class CategoryPager extends AlphabeticPager {
-       function __construct( $from ) {
-               parent::__construct();
-               $from = str_replace( ' ', '_', $from );
-               if( $from !== '' ) {
-                       global $wgCapitalLinks, $wgContLang;
-                       if( $wgCapitalLinks ) {
-                               $from = $wgContLang->ucfirst( $from );
-                       }
-                       $this->mOffset = $from;
-               }
-       }
-       
-       function getQueryInfo() {
-               global $wgRequest;
-               return array(
-                       'tables' => array( 'category' ),
-                       'fields' => array( 'cat_title','cat_pages' ),
-                       'conds' => array( 'cat_pages > 0' ), 
-                       'options' => array( 'USE INDEX' => 'cat_title' ),
-               );
-       }
-
-       function getIndexField() {
-#              return array( 'abc' => 'cat_title', 'count' => 'cat_pages' );
-               return 'cat_title';
-       }
-
-       function getDefaultQuery() {
-               parent::getDefaultQuery();
-               unset( $this->mDefaultQuery['from'] );
-       }
-#      protected function getOrderTypeMessages() {
-#              return array( 'abc' => 'special-categories-sort-abc',
-#                      'count' => 'special-categories-sort-count' );
-#      }
-
-       protected function getDefaultDirections() {
-#              return array( 'abc' => false, 'count' => true );
-               return false;
-       }
-
-       /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */
-       public function getBody() {
-               if (!$this->mQueryDone) {
-                       $this->doQuery();
-               }
-               $batch = new LinkBatch;
-
-               $this->mResult->rewind();
-
-               while ( $row = $this->mResult->fetchObject() ) {
-                       $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cat_title ) );
-               }
-               $batch->execute();
-               $this->mResult->rewind();
-               return parent::getBody();
-       }
-
-       function formatRow($result) {
-               global $wgLang;
-               $title = Title::makeTitle( NS_CATEGORY, $result->cat_title );
-               $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
-               $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ),
-                               $wgLang->formatNum( $result->cat_pages ) );
-               return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
-       }
-       
-       public function getStartForm( $from ) {
-               global $wgScript;
-               $t = SpecialPage::getTitleFor( 'Categories' );
-       
-               return
-                       Xml::tags( 'form', array( 'method' => 'get', 'action' => $wgScript ),
-                               Xml::hidden( 'title', $t->getPrefixedText() ) .
-                               Xml::fieldset( wfMsg( 'categories' ),
-                                       Xml::inputLabel( wfMsg( 'categoriesfrom' ),
-                                               'from', 'from', 20, $from ) .
-                                       ' ' .
-                                       Xml::submitButton( wfMsg( 'allpagessubmit' ) ) ) );
-       }
-}
diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php
deleted file mode 100644 (file)
index 9075fb9..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-<?php
-
-/**
- * Special page allows users to request email confirmation message, and handles
- * processing of the confirmation code when the link in the email is followed
- *
- * @ingroup SpecialPage
- * @author Brion Vibber
- * @author Rob Church <robchur@gmail.com>
- */
-class EmailConfirmation extends UnlistedSpecialPage {
-
-       /**
-        * Constructor
-        */
-       public function __construct() {
-               parent::__construct( 'Confirmemail' );
-       }
-
-       /**
-        * Main execution point
-        *
-        * @param $code Confirmation code passed to the page
-        */
-       function execute( $code ) {
-               global $wgUser, $wgOut;
-               $this->setHeaders();
-               if( empty( $code ) ) {
-                       if( $wgUser->isLoggedIn() ) {
-                               if( User::isValidEmailAddr( $wgUser->getEmail() ) ) {
-                                       $this->showRequestForm();
-                               } else {
-                                       $wgOut->addWikiMsg( 'confirmemail_noemail' );
-                               }
-                       } else {
-                               $title = SpecialPage::getTitleFor( 'Userlogin' );
-                               $self = SpecialPage::getTitleFor( 'Confirmemail' );
-                               $skin = $wgUser->getSkin();
-                               $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() );
-                               $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
-                       }
-               } else {
-                       $this->attemptConfirm( $code );
-               }
-       }
-
-       /**
-        * Show a nice form for the user to request a confirmation mail
-        */
-       function showRequestForm() {
-               global $wgOut, $wgUser, $wgLang, $wgRequest;
-               if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
-                       $ok = $wgUser->sendConfirmationMail();
-                       if ( WikiError::isError( $ok ) ) {
-                               $wgOut->addWikiMsg( 'confirmemail_sendfailed', $ok->toString() );
-                       } else {
-                               $wgOut->addWikiMsg( 'confirmemail_sent' );
-                       }
-               } else {
-                       if( $wgUser->isEmailConfirmed() ) {
-                               $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
-                               $wgOut->addWikiMsg( 'emailauthenticated', $time );
-                       }
-                       if( $wgUser->isEmailConfirmationPending() ) {
-                               $wgOut->addWikiMsg( 'confirmemail_pending' );
-                       }
-                       $wgOut->addWikiMsg( 'confirmemail_text' );
-                       $self = SpecialPage::getTitleFor( 'Confirmemail' );
-                       $form  = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
-                       $form .= wfHidden( 'token', $wgUser->editToken() );
-                       $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) );
-                       $form .= wfCloseElement( 'form' );
-                       $wgOut->addHtml( $form );
-               }
-       }
-
-       /**
-        * Attempt to confirm the user's email address and show success or failure
-        * as needed; if successful, take the user to log in
-        *
-        * @param $code Confirmation code
-        */
-       function attemptConfirm( $code ) {
-               global $wgUser, $wgOut;
-               $user = User::newFromConfirmationCode( $code );
-               if( is_object( $user ) ) {
-                       $user->confirmEmail();
-                       $user->saveSettings();
-                       $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
-                       $wgOut->addWikiMsg( $message );
-                       if( !$wgUser->isLoggedIn() ) {
-                               $title = SpecialPage::getTitleFor( 'Userlogin' );
-                               $wgOut->returnToMain( true, $title );
-                       }
-               } else {
-                       $wgOut->addWikiMsg( 'confirmemail_invalid' );
-               }
-       }
-
-}
-
-/**
- * Special page allows users to cancel an email confirmation using the e-mail
- * confirmation code
- *
- * @ingroup SpecialPage
- */
-class EmailInvalidation extends UnlistedSpecialPage {
-
-       public function __construct() {
-               parent::__construct( 'Invalidateemail' );
-       }
-
-       function execute( $code ) {
-               $this->setHeaders();
-               $this->attemptInvalidate( $code );
-       }
-
-       /**
-        * Attempt to invalidate the user's email address and show success or failure
-        * as needed; if successful, link to main page
-        *
-        * @param $code Confirmation code
-        */
-       function attemptInvalidate( $code ) {
-               global $wgUser, $wgOut;
-               $user = User::newFromConfirmationCode( $code );
-               if( is_object( $user ) ) {
-                       $user->invalidateEmail();
-                       $user->saveSettings();
-                       $wgOut->addWikiMsg( 'confirmemail_invalidated' );
-                       if( !$wgUser->isLoggedIn() ) {
-                               $wgOut->returnToMain();
-                       }
-               } else {
-                       $wgOut->addWikiMsg( 'confirmemail_invalid' );
-               }
-       }
-}
diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php
deleted file mode 100644 (file)
index c9cbc18..0000000
+++ /dev/null
@@ -1,465 +0,0 @@
-<?php
-/**
- * Special:Contributions, show user contributions in a paged list
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Pager for Special:Contributions
- * @ingroup SpecialPage Pager
- */
-class ContribsPager extends ReverseChronologicalPager {
-       public $mDefaultDirection = true;
-       var $messages, $target;
-       var $namespace = '', $year = '', $month = '', $mDb;
-
-       function __construct( $target, $namespace = false, $year = false, $month = false ) {
-               parent::__construct();
-               foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
-                       $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
-               }
-               $this->target = $target;
-               $this->namespace = $namespace;
-
-               $year = intval($year);
-               $month = intval($month);
-
-               $this->year = $year > 0 ? $year : false;
-               $this->month = ($month > 0 && $month < 13) ? $month : false;
-               $this->getDateCond();
-
-               $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
-       }
-
-       function getDefaultQuery() {
-               $query = parent::getDefaultQuery();
-               $query['target'] = $this->target;
-               $query['month'] = $this->month;
-               $query['year'] = $this->year;
-               return $query;
-       }
-
-       function getQueryInfo() {
-               list( $index, $userCond ) = $this->getUserCond();
-               $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
-               return array(
-                       'tables' => array( 'page', 'revision' ),
-                       'fields' => array(
-                               'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'rev_id', 'rev_page',
-                               'rev_text_id', 'rev_timestamp', 'rev_comment', 'rev_minor_edit', 'rev_user',
-                               'rev_user_text', 'rev_parent_id', 'rev_deleted'
-                       ),
-                       'conds' => $conds,
-                       'options' => array( 'USE INDEX' => $index )
-               );
-       }
-
-       function getUserCond() {
-               $condition = array();
-
-               if ( $this->target == 'newbies' ) {
-                       $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
-                       $condition[] = 'rev_user >' . (int)($max - $max / 100);
-                       $index = 'user_timestamp';
-               } else {
-                       $condition['rev_user_text'] = $this->target;
-                       $index = 'usertext_timestamp';
-               }
-               return array( $index, $condition );
-       }
-
-       function getNamespaceCond() {
-               if ( $this->namespace !== '' ) {
-                       return array( 'page_namespace' => (int)$this->namespace );
-               } else {
-                       return array();
-               }
-       }
-
-       function getDateCond() {
-               // Given an optional year and month, we need to generate a timestamp
-               // to use as "WHERE rev_timestamp <= result"
-               // Examples: year = 2006 equals < 20070101 (+000000)
-               // year=2005, month=1    equals < 20050201
-               // year=2005, month=12   equals < 20060101
-
-               if (!$this->year && !$this->month)
-                       return;
-
-               if ( $this->year ) {
-                       $year = $this->year;
-               }
-               else {
-                       // If no year given, assume the current one
-                       $year = gmdate( 'Y' );
-                       // If this month hasn't happened yet this year, go back to last year's month
-                       if( $this->month > gmdate( 'n' ) ) {
-                               $year--;
-                       }
-               }
-
-               if ( $this->month ) {
-                       $month = $this->month + 1;
-                       // For December, we want January 1 of the next year
-                       if ($month > 12) {
-                               $month = 1;
-                               $year++;
-                       }
-               }
-               else {
-                       // No month implies we want up to the end of the year in question
-                       $month = 1;
-                       $year++;
-               }
-
-               if ($year > 2032)
-                       $year = 2032;
-               $ymd = (int)sprintf( "%04d%02d01", $year, $month );
-
-               // Y2K38 bug
-               if ($ymd > 20320101)
-                       $ymd = 20320101;
-
-               $this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
-       }
-
-       function getIndexField() {
-               return 'rev_timestamp';
-       }
-
-       function getStartBody() {
-               return "<ul>\n";
-       }
-
-       function getEndBody() {
-               return "</ul>\n";
-       }
-
-       /**
-        * Generates each row in the contributions list.
-        *
-        * Contributions which are marked "top" are currently on top of the history.
-        * For these contributions, a [rollback] link is shown for users with roll-
-        * back privileges. The rollback link restores the most recent version that
-        * was not written by the target user.
-        *
-        * @todo This would probably look a lot nicer in a table.
-        */
-       function formatRow( $row ) {
-               wfProfileIn( __METHOD__ );
-
-               global $wgLang, $wgUser, $wgContLang;
-
-               $sk = $this->getSkin();
-               $rev = new Revision( $row );
-
-               $page = Title::makeTitle( $row->page_namespace, $row->page_title );
-               $link = $sk->makeKnownLinkObj( $page );
-               $difftext = $topmarktext = '';
-               if( $row->rev_id == $row->page_latest ) {
-                       $topmarktext .= '<strong>' . $this->messages['uctop'] . '</strong>';
-                       if( !$row->page_is_new ) {
-                               $difftext .= '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=0' ) . ')';
-                       } else {
-                               $difftext .= $this->messages['newarticle'];
-                       }
-
-                       if( !$page->getUserPermissionsErrors( 'rollback', $wgUser )
-                       &&  !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) {
-                               $topmarktext .= ' '.$sk->generateRollback( $rev );
-                       }
-
-               }
-               # Is there a visible previous revision?
-               if( $rev->userCan(Revision::DELETED_TEXT) ) {
-                       $difftext = '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
-               } else {
-                       $difftext = '(' . $this->messages['diff'] . ')';
-               }
-               $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
-
-               $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
-               $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
-
-               if( $this->target == 'newbies' ) {
-                       $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
-                       $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') ';
-               } else {
-                       $userlink = '';
-               }
-
-               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
-                       $d = '<span class="history-deleted">' . $d . '</span>';
-               }
-
-               if( $rev->getParentId() === 0 ) {
-                       $nflag = '<span class="newpage">' . $this->messages['newpageletter'] . '</span>';
-               } else {
-                       $nflag = '';
-               }
-
-               if( $row->rev_minor_edit ) {
-                       $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
-               } else {
-                       $mflag = '';
-               }
-
-               $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink}{$comment} {$topmarktext}";
-               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
-                       $ret .= ' ' . wfMsgHtml( 'deletedrev' );
-               }
-               $ret = "<li>$ret</li>\n";
-               wfProfileOut( __METHOD__ );
-               return $ret;
-       }
-
-       /**
-        * Get the Database object in use
-        *
-        * @return Database
-        */
-       public function getDatabase() {
-               return $this->mDb;
-       }
-
-}
-
-/**
- * Special page "user contributions".
- * Shows a list of the contributions of a user.
- *
- * @return     none
- * @param      $par    String: (optional) user name of the user for which to show the contributions
- */
-function wfSpecialContributions( $par = null ) {
-       global $wgUser, $wgOut, $wgLang, $wgRequest;
-
-       $options = array();
-
-       if ( isset( $par ) && $par == 'newbies' ) {
-               $target = 'newbies';
-               $options['contribs'] = 'newbie';
-       } elseif ( isset( $par ) ) {
-               $target = $par;
-       } else {
-               $target = $wgRequest->getVal( 'target' );
-       }
-
-       // check for radiobox
-       if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
-               $target = 'newbies';
-               $options['contribs'] = 'newbie';
-       }
-
-       if ( !strlen( $target ) ) {
-               $wgOut->addHTML( contributionsForm( '' ) );
-               return;
-       }
-
-       $options['limit'] = $wgRequest->getInt( 'limit', 50 );
-       $options['target'] = $target;
-
-       $nt = Title::makeTitleSafe( NS_USER, $target );
-       if ( !$nt ) {
-               $wgOut->addHTML( contributionsForm( '' ) );
-               return;
-       }
-       $id = User::idFromName( $nt->getText() );
-
-       if ( $target != 'newbies' ) {
-               $target = $nt->getText();
-               $wgOut->setSubtitle( contributionsSub( $nt, $id ) );
-       } else {
-               $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
-       }
-
-       if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
-               $options['namespace'] = intval( $ns );
-       } else {
-               $options['namespace'] = '';
-       }
-       if ( $wgUser->isAllowed( 'markbotedit' ) && $wgRequest->getBool( 'bot' ) ) {
-               $options['bot'] = '1';
-       }
-
-       $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
-       # Offset overrides year/month selection
-       if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) {
-               $options['month'] = intval( $month );
-       } else {
-               $options['month'] = '';
-       }
-       if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) {
-               $options['year'] = intval( $year );
-       } else if( $options['month'] ) {
-               $thisMonth = intval( gmdate( 'n' ) );
-               $thisYear = intval( gmdate( 'Y' ) );
-               if( intval( $options['month'] ) > $thisMonth ) {
-                       $thisYear--;
-               }
-               $options['year'] = $thisYear;
-       } else {
-               $options['year'] = '';
-       }
-
-       wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
-
-       if( $skip ) {
-               $options['year'] = '';
-               $options['month'] = '';
-       }
-
-       $wgOut->addHTML( contributionsForm( $options ) );
-
-       $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] );
-       if ( !$pager->getNumRows() ) {
-               $wgOut->addWikiMsg( 'nocontribs' );
-               return;
-       }
-
-       # Show a message about slave lag, if applicable
-       if( ( $lag = $pager->getDatabase()->getLag() ) > 0 )
-               $wgOut->showLagWarning( $lag );
-
-       $wgOut->addHTML(
-               '<p>' . $pager->getNavigationBar() . '</p>' .
-               $pager->getBody() .
-               '<p>' . $pager->getNavigationBar() . '</p>' );
-
-       # If there were contributions, and it was a valid user or IP, show
-       # the appropriate "footer" message - WHOIS tools, etc.
-       if( $target != 'newbies' ) {
-               $message = IP::isIPAddress( $target )
-                       ? 'sp-contributions-footer-anon'
-                       : 'sp-contributions-footer';
-
-
-               $text = wfMsgNoTrans( $message, $target );
-               if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
-                       $wgOut->addHtml( '<div class="mw-contributions-footer">' );
-                       $wgOut->addWikiText( $text );
-                       $wgOut->addHtml( '</div>' );
-               }
-       }
-}
-
-/**
- * Generates the subheading with links
- * @param Title $nt Title object for the target
- * @param integer $id User ID for the target
- * @return String: appropriately-escaped HTML to be output literally
- */
-function contributionsSub( $nt, $id ) {
-       global $wgSysopUserBans, $wgLang, $wgUser;
-
-       $sk = $wgUser->getSkin();
-
-       if ( 0 == $id ) {
-               $user = $nt->getText();
-       } else {
-               $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
-       }
-       $talk = $nt->getTalkPage();
-       if( $talk ) {
-               # Talk page link
-               $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
-               if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) {
-                       # Block link
-                       if( $wgUser->isAllowed( 'block' ) )
-                               $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
-                       # Block log link
-                       $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
-               }
-               # Other logs link
-               $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
-
-               wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
-
-               $links = implode( ' | ', $tools );
-       }
-
-       // Old message 'contribsub' had one parameter, but that doesn't work for
-       // languages that want to put the "for" bit right after $user but before
-       // $links.  If 'contribsub' is around, use it for reverse compatibility,
-       // otherwise use 'contribsub2'.
-       if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
-               return wfMsgHtml( 'contribsub2', $user, $links );
-       } else {
-               return wfMsgHtml( 'contribsub', "$user ($links)" );
-       }
-}
-
-/**
- * Generates the namespace selector form with hidden attributes.
- * @param $options Array: the options to be included.
- */
-function contributionsForm( $options ) {
-       global $wgScript, $wgTitle, $wgRequest;
-
-       $options['title'] = $wgTitle->getPrefixedText();
-       if ( !isset( $options['target'] ) ) {
-               $options['target'] = '';
-       } else {
-               $options['target'] = str_replace( '_' , ' ' , $options['target'] );
-       }
-
-       if ( !isset( $options['namespace'] ) ) {
-               $options['namespace'] = '';
-       }
-
-       if ( !isset( $options['contribs'] ) ) {
-               $options['contribs'] = 'user';
-       }
-
-       if ( !isset( $options['year'] ) ) {
-               $options['year'] = '';
-       }
-
-       if ( !isset( $options['month'] ) ) {
-               $options['month'] = '';
-       }
-
-       if ( $options['contribs'] == 'newbie' ) {
-               $options['target'] = '';
-       }
-
-       $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
-
-       foreach ( $options as $name => $value ) {
-               if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
-                       continue;
-               }
-               $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
-       }
-
-       $f .= '<fieldset>' .
-               Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
-               Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '<br />' .
-               Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' .
-               Xml::input( 'target', 20, $options['target']) . ' '.
-               '<span style="white-space: nowrap">' .
-               Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
-               Xml::namespaceSelector( $options['namespace'], '' ) .
-               '</span>' .
-               Xml::openElement( 'p' ) .
-               '<span style="white-space: nowrap">' .
-               Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
-               Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) .
-               '</span>' .
-               ' '.
-               '<span style="white-space: nowrap">' .
-               Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
-               Xml::monthSelector( $options['month'], -1 ) . ' '.
-               '</span>' .
-               Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
-               Xml::closeElement( 'p' );
-
-       $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' );
-       if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) )
-               $f .= "<p>{$explain}</p>";
-
-       $f .= '</fieldset>' .
-               Xml::closeElement( 'form' );
-       return $f;
-}
diff --git a/includes/SpecialDeadendpages.php b/includes/SpecialDeadendpages.php
deleted file mode 100644 (file)
index a8416c9..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @ingroup SpecialPage
- */
-class DeadendPagesPage extends PageQueryPage {
-
-       function getName( ) {
-               return "Deadendpages";
-       }
-
-       function getPageHeader() {
-               return wfMsgExt( 'deadendpagestext', array( 'parse' ) );
-       }
-
-       /**
-        * LEFT JOIN is expensive
-        *
-        * @return true
-        */
-       function isExpensive( ) {
-               return 1;
-       }
-
-       function isSyndicated() { return false; }
-
-       /**
-        * @return false
-        */
-       function sortDescending() {
-               return false;
-       }
-
-       /**
-        * @return string an sqlquery
-        */
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
-               return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " .
-       "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " .
-       "WHERE pl_from IS NULL " .
-       "AND page_namespace = 0 " .
-       "AND page_is_redirect = 0";
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialDeadendpages() {
-
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $depp = new DeadendPagesPage();
-
-       return $depp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialDisambiguations.php b/includes/SpecialDisambiguations.php
deleted file mode 100644 (file)
index 3404566..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @ingroup SpecialPage
- */
-class DisambiguationsPage extends PageQueryPage {
-
-       function getName() {
-               return 'Disambiguations';
-       }
-
-       function isExpensive( ) { return true; }
-       function isSyndicated() { return false; }
-
-
-       function getPageHeader( ) {
-               return wfMsgExt( 'disambiguations-text', array( 'parse' ) );
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-
-               $dMsgText = wfMsgForContent('disambiguationspage');
-
-               $linkBatch = new LinkBatch;
-
-               # If the text can be treated as a title, use it verbatim.
-               # Otherwise, pull the titles from the links table
-               $dp = Title::newFromText($dMsgText);
-               if( $dp ) {
-                       if($dp->getNamespace() != NS_TEMPLATE) {
-                               # FIXME we assume the disambiguation message is a template but
-                               # the page can potentially be from another namespace :/
-                               wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n");
-                       }
-                       $linkBatch->addObj( $dp );
-               } else {
-                               # Get all the templates linked from the Mediawiki:Disambiguationspage
-                               $disPageObj = Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage' );
-                               $res = $dbr->select(
-                                       array('pagelinks', 'page'),
-                                       'pl_title',
-                                       array('page_id = pl_from', 'pl_namespace' => NS_TEMPLATE,
-                                               'page_namespace' => $disPageObj->getNamespace(), 'page_title' => $disPageObj->getDBkey()),
-                                       __METHOD__ );
-
-                               while ( $row = $dbr->fetchObject( $res ) ) {
-                                       $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title ));
-                               }
-
-                               $dbr->freeResult( $res );
-               }
-
-               $set = $linkBatch->constructSet( 'lb.tl', $dbr );
-               if( $set === false ) {
-                       # We must always return a valid sql query, but this way DB will always quicly return an empty result
-                       $set = 'FALSE';
-                       wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n");
-               }
-
-               list( $page, $pagelinks, $templatelinks) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' );
-
-               $sql = "SELECT 'Disambiguations' AS \"type\", pb.page_namespace AS namespace,"
-                       ." pb.page_title AS title, la.pl_from AS value"
-                       ." FROM {$templatelinks} AS lb, {$page} AS pb, {$pagelinks} AS la, {$page} AS pa"
-                       ." WHERE $set"  # disambiguation template(s)
-                       .' AND pa.page_id = la.pl_from'
-                       .' AND pa.page_namespace = ' . NS_MAIN  # Limit to just articles in the main namespace
-                       .' AND pb.page_id = lb.tl_from'
-                       .' AND pb.page_namespace = la.pl_namespace'
-                       .' AND pb.page_title = la.pl_title'
-                       .' ORDER BY lb.tl_namespace, lb.tl_title';
-
-               return $sql;
-       }
-
-       function getOrder() {
-               return '';
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgContLang;
-               $title = Title::newFromId( $result->value );
-               $dp = Title::makeTitle( $result->namespace, $result->title );
-
-               $from = $skin->makeKnownLinkObj( $title, '' );
-               $edit = $skin->makeKnownLinkObj( $title, "(".wfMsgHtml("qbedit").")" , 'redirect=no&action=edit' );
-               $arr  = $wgContLang->getArrow();
-               $to   = $skin->makeKnownLinkObj( $dp, '' );
-
-               return "$from $edit $arr $to";
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialDisambiguations() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $sd = new DisambiguationsPage();
-
-       return $sd->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php
deleted file mode 100644 (file)
index b1bad0c..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page listing redirects to redirecting page.
- * The software will automatically not follow double redirects, to prevent loops.
- * @ingroup SpecialPage
- */
-class DoubleRedirectsPage extends PageQueryPage {
-
-       function getName() {
-               return 'DoubleRedirects';
-       }
-
-       function isExpensive( ) { return true; }
-       function isSyndicated() { return false; }
-
-       function getPageHeader( ) {
-               return wfMsgExt( 'doubleredirectstext', array( 'parse' ) );
-       }
-
-       function getSQLText( &$dbr, $namespace = null, $title = null ) {
-
-               list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
-
-               $limitToTitle = !( $namespace === null && $title === null );
-               $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ;
-               $sql .=
-                        " pa.page_namespace as namespace, pa.page_title as title," .
-                        " pb.page_namespace as nsb, pb.page_title as tb," .
-                        " pc.page_namespace as nsc, pc.page_title as tc" .
-                  " FROM $redirect AS ra, $redirect AS rb, $page AS pa, $page AS pb, $page AS pc" .
-                  " WHERE ra.rd_from=pa.page_id" .
-                        " AND ra.rd_namespace=pb.page_namespace" .
-                        " AND ra.rd_title=pb.page_title" .
-                        " AND rb.rd_from=pb.page_id" .
-                        " AND rb.rd_namespace=pc.page_namespace" .
-                        " AND rb.rd_title=pc.page_title";
-
-               if( $limitToTitle ) {
-                       $encTitle = $dbr->addQuotes( $title );
-                       $sql .= " AND pa.page_namespace=$namespace" .
-                                       " AND pa.page_title=$encTitle";
-               }
-
-               return $sql;
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               return $this->getSQLText( $dbr );
-       }
-
-       function getOrder() {
-               return '';
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgContLang;
-
-               $fname = 'DoubleRedirectsPage::formatResult';
-               $titleA = Title::makeTitle( $result->namespace, $result->title );
-
-               if ( $result && !isset( $result->nsb ) ) {
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $sql = $this->getSQLText( $dbr, $result->namespace, $result->title );
-                       $res = $dbr->query( $sql, $fname );
-                       if ( $res ) {
-                               $result = $dbr->fetchObject( $res );
-                               $dbr->freeResult( $res );
-                       }
-               }
-               if ( !$result ) {
-                       return '<s>' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . '</s>';
-               }
-
-               $titleB = Title::makeTitle( $result->nsb, $result->tb );
-               $titleC = Title::makeTitle( $result->nsc, $result->tc );
-
-               $linkA = $skin->makeKnownLinkObj( $titleA, '', 'redirect=no' );
-               $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no');
-               $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' );
-               $linkC = $skin->makeKnownLinkObj( $titleC );
-               $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
-
-               return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialDoubleRedirects() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $sdr = new DoubleRedirectsPage();
-
-       return $sdr->doQuery( $offset, $limit );
-
-}
diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php
deleted file mode 100644 (file)
index c06d7a5..0000000
+++ /dev/null
@@ -1,282 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @todo document
- */
-function wfSpecialEmailuser( $par ) {
-       global $wgRequest, $wgUser, $wgOut;
-
-       $action = $wgRequest->getVal( 'action' );
-       $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
-       $targetUser = EmailUserForm::validateEmailTarget( $target );
-       
-       if ( !( $targetUser instanceof User ) ) {
-               $wgOut->showErrorPage( $targetUser[0], $targetUser[1] );
-               return;
-       }
-       
-       $form = new EmailUserForm( $targetUser,
-                       $wgRequest->getText( 'wpText' ),
-                       $wgRequest->getText( 'wpSubject' ),
-                       $wgRequest->getBool( 'wpCCMe' ) );
-       if ( $action == 'success' ) {
-               $form->showSuccess();
-               return;
-       }
-                                       
-       $error = EmailUserForm::getPermissionsError( $wgUser, $wgRequest->getVal( 'wpEditToken' ) );
-       if ( $error ) {
-               switch ( $error[0] ) {
-                       case 'blockedemailuser':
-                               $wgOut->blockedPage();
-                               return;
-                       case 'actionthrottledtext':
-                               $wgOut->rateLimited();
-                               return;
-                       case 'sessionfailure':
-                               $form->showForm();
-                               return;
-                       default:
-                               $wgOut->showErrorPage( $error[0], $error[1] );
-                               return;
-               }
-       }       
-               
-       
-       if ( "submit" == $action && $wgRequest->wasPosted() ) {
-               $result = $form->doSubmit();
-               
-               if ( !is_null( $result ) ) {
-                       $wgOut->addHTML( wfMsg( "usermailererror" ) .
-                                       ' ' . htmlspecialchars( $result->getMessage() ) );
-               } else {
-                       $titleObj = SpecialPage::getTitleFor( "Emailuser" );
-                       $encTarget = wfUrlencode( $form->getTarget()->getName() );
-                       $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) );
-               }
-       } else {
-               $form->showForm();
-       }
-}
-
-/**
- * Implements the Special:Emailuser web interface, and invokes userMailer for sending the email message.
- * @ingroup SpecialPage
- */
-class EmailUserForm {
-
-       var $target;
-       var $text, $subject;
-       var $cc_me;     // Whether user requested to be sent a separate copy of their email.
-
-       /**
-        * @param User $target
-        */
-       function EmailUserForm( $target, $text, $subject, $cc_me ) {
-               $this->target = $target;
-               $this->text = $text;
-               $this->subject = $subject;
-               $this->cc_me = $cc_me;
-       }
-
-       function showForm() {
-               global $wgOut, $wgUser;
-               $skin = $wgUser->getSkin();
-
-               $wgOut->setPagetitle( wfMsg( "emailpage" ) );
-               $wgOut->addWikiMsg( "emailpagetext" );
-
-               if ( $this->subject === "" ) {
-                       $this->subject = wfMsgForContent( "defemailsubject" );
-               }
-
-               $emf = wfMsg( "emailfrom" );
-               $senderLink = $skin->makeLinkObj(
-                       $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) );
-               $emt = wfMsg( "emailto" );
-               $recipientLink = $skin->makeLinkObj(
-                       $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) );
-               $emr = wfMsg( "emailsubject" );
-               $emm = wfMsg( "emailmessage" );
-               $ems = wfMsg( "emailsend" );
-               $emc = wfMsg( "emailccme" );
-               $encSubject = htmlspecialchars( $this->subject );
-
-               $titleObj = SpecialPage::getTitleFor( "Emailuser" );
-               $action = $titleObj->escapeLocalURL( "target=" .
-                       urlencode( $this->target->getName() ) . "&action=submit" );
-               $token = htmlspecialchars( $wgUser->editToken() );
-
-               $wgOut->addHTML( "
-<form id=\"emailuser\" method=\"post\" action=\"{$action}\">
-<table border='0' id='mailheader'><tr>
-<td align='right'>{$emf}:</td>
-<td align='left'><strong>{$senderLink}</strong></td>
-</tr><tr>
-<td align='right'>{$emt}:</td>
-<td align='left'><strong>{$recipientLink}</strong></td>
-</tr><tr>
-<td align='right'>{$emr}:</td>
-<td align='left'>
-<input type='text' size='60' maxlength='200' name=\"wpSubject\" value=\"{$encSubject}\" />
-</td>
-</tr>
-</table>
-<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span>
-<textarea id=\"wpText\" name=\"wpText\" rows='20' cols='80' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
-"</textarea>
-" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br />
-<input type='submit' name=\"wpSend\" value=\"{$ems}\" />
-<input type='hidden' name='wpEditToken' value=\"$token\" />
-</form>\n" );
-
-       }
-
-       /*
-        * Really send a mail. Permissions should have been checked using 
-        * EmailUserForm::getPermissionsError. It is probably also a good idea to
-        * check the edit token and ping limiter in advance.
-        */
-       function doSubmit() {
-               global $wgUser, $wgUserEmailUseReplyTo;
-
-               $to = new MailAddress( $this->target );
-               $from = new MailAddress( $wgUser );
-               $subject = $this->subject;
-
-               if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) {
-
-                       if( $wgUserEmailUseReplyTo ) {
-                               // Put the generic wiki autogenerated address in the From:
-                               // header and reserve the user for Reply-To.
-                               //
-                               // This is a bit ugly, but will serve to differentiate
-                               // wiki-borne mails from direct mails and protects against
-                               // SPF and bounce problems with some mailers (see below).
-                               global $wgPasswordSender;
-                               $mailFrom = new MailAddress( $wgPasswordSender );
-                               $replyTo = $from;
-                       } else {
-                               // Put the sending user's e-mail address in the From: header.
-                               //
-                               // This is clean-looking and convenient, but has issues.
-                               // One is that it doesn't as clearly differentiate the wiki mail
-                               // from "directly" sent mails.
-                               //
-                               // Another is that some mailers (like sSMTP) will use the From
-                               // address as the envelope sender as well. For open sites this
-                               // can cause mails to be flunked for SPF violations (since the
-                               // wiki server isn't an authorized sender for various users'
-                               // domains) as well as creating a privacy issue as bounces
-                               // containing the recipient's e-mail address may get sent to
-                               // the sending user.
-                               $mailFrom = $from;
-                               $replyTo = null;
-                       }
-                       
-                       $mailResult = UserMailer::send( $to, $mailFrom, $subject, $this->text, $replyTo );
-
-                       if( WikiError::isError( $mailResult ) ) {
-                               return $mailResult;                     
-                               
-                       } else {
-
-                               // if the user requested a copy of this mail, do this now,
-                               // unless they are emailing themselves, in which case one copy of the message is sufficient.
-                               if ($this->cc_me && $to != $from) {
-                                       $cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject);
-                                       if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) {
-                                               $ccResult = UserMailer::send( $from, $from, $cc_subject, $this->text );
-                                               if( WikiError::isError( $ccResult ) ) {
-                                                       // At this stage, the user's CC mail has failed, but their
-                                                       // original mail has succeeded. It's unlikely, but still, what to do?
-                                                       // We can either show them an error, or we can say everything was fine,
-                                                       // or we can say we sort of failed AND sort of succeeded. Of these options,
-                                                       // simply saying there was an error is probably best.
-                                                       return $ccResult;
-                                               }
-                                       }
-                               }
-
-                               wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) );
-                               return;
-                       }
-               }
-       }
-
-       function showSuccess( &$user = null ) {
-               global $wgOut;
-               
-               if ( is_null($user) )
-                       $user = $this->target;
-
-               $wgOut->setPagetitle( wfMsg( "emailsent" ) );
-               $wgOut->addHTML( wfMsg( "emailsenttext" ) );
-
-               $wgOut->returnToMain( false, $user->getUserPage() );
-       }
-       
-       function getTarget() {
-               return $this->target;
-       }
-       
-       static function validateEmailTarget ( $target ) {
-               global $wgEnableEmail, $wgEnableUserEmail;
-
-               if( !( $wgEnableEmail && $wgEnableUserEmail ) ) 
-                       return array( "nosuchspecialpage", "nospecialpagetext" );
-               
-               if ( "" == $target ) {
-                       wfDebug( "Target is empty.\n" );
-                       return array( "notargettitle", "notargettext" );
-               }
-       
-               $nt = Title::newFromURL( $target );
-               if ( is_null( $nt ) ) {
-                       wfDebug( "Target is invalid title.\n" );
-                       return array( "notargettitle", "notargettext" );
-               }
-       
-               $nu = User::newFromName( $nt->getText() );
-               if( is_null( $nu ) || !$nu->canReceiveEmail() ) {
-                       wfDebug( "Target is invalid user or can't receive.\n" );
-                       return array( "noemailtitle", "noemailtext" );
-               }
-               
-               return $nu;
-       }
-       static function getPermissionsError ( $user, $editToken ) {
-               if( !$user->canSendEmail() ) {
-                       wfDebug( "User can't send.\n" );
-                       return array( "mailnologin", "mailnologintext" );
-               }
-               
-               if( $user->isBlockedFromEmailuser() ) {
-                       wfDebug( "User is blocked from sending e-mail.\n" );
-                       return array( "blockedemailuser", "" );
-               }
-               
-               if( $user->pingLimiter( 'emailuser' ) ) {
-                       wfDebug( "Ping limiter triggered.\n" ); 
-                       return array( 'actionthrottledtext', '' );
-               }
-               
-               if( !$user->matchEditToken( $editToken ) ) {
-                       wfDebug( "Matching edit token failed.\n" );
-                       return array( 'sessionfailure', '' );
-               }
-               
-               return;
-       }
-       
-       static function newFromURL( $target, $text, $subject, $cc_me )
-       {
-               $nt = Title::newFromURL( $target );
-               $nu = User::newFromName( $nt->getText() );
-               return new EmailUserForm( $nu, $text, $subject, $cc_me );
-       }
-}
diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php
deleted file mode 100644 (file)
index 38bfc83..0000000
+++ /dev/null
@@ -1,284 +0,0 @@
-<?php
-# Copyright (C) 2003 Brion Vibber <brion@pobox.com>
-# http://www.mediawiki.org/
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# http://www.gnu.org/copyleft/gpl.html
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-function wfExportGetPagesFromCategory( $title ) {
-       global $wgContLang;
-
-       $name = $title->getDBkey();
-
-       $dbr = wfGetDB( DB_SLAVE );
-
-       list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
-       $sql = "SELECT page_namespace, page_title FROM $page " .
-               "JOIN $categorylinks ON cl_from = page_id " .
-               "WHERE cl_to = " . $dbr->addQuotes( $name );
-
-       $pages = array();
-       $res = $dbr->query( $sql, 'wfExportGetPagesFromCategory' );
-       while ( $row = $dbr->fetchObject( $res ) ) {
-               $n = $row->page_title;
-               if ($row->page_namespace) {
-                       $ns = $wgContLang->getNsText( $row->page_namespace );
-                       $n = $ns . ':' . $n;
-               }
-
-               $pages[] = $n;
-       }
-       $dbr->freeResult($res);
-
-       return $pages;
-}
-
-/**
- * Expand a list of pages to include templates used in those pages.
- * @param $inputPages array, list of titles to look up
- * @param $pageSet array, associative array indexed by titles for output
- * @return array associative array index by titles
- */
-function wfExportGetTemplates( $inputPages, $pageSet ) {
-       return wfExportGetLinks( $inputPages, $pageSet,
-               'templatelinks',
-               array( 'tl_namespace AS namespace', 'tl_title AS title' ),
-               array( 'page_id=tl_from' ) );
-}
-
-/**
- * Expand a list of pages to include images used in those pages.
- * @param $inputPages array, list of titles to look up
- * @param $pageSet array, associative array indexed by titles for output
- * @return array associative array index by titles
- */
-function wfExportGetImages( $inputPages, $pageSet ) {
-       return wfExportGetLinks( $inputPages, $pageSet,
-               'imagelinks',
-               array( NS_IMAGE . ' AS namespace', 'il_to AS title' ),
-               array( 'page_id=il_from' ) );
-}
-
-/**
- * Expand a list of pages to include items used in those pages.
- * @private
- */
-function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) {
-       $dbr = wfGetDB( DB_SLAVE );
-       foreach( $inputPages as $page ) {
-               $title = Title::newFromText( $page );
-               if( $title ) {
-                       $pageSet[$title->getPrefixedText()] = true;
-                       /// @fixme May or may not be more efficient to batch these
-                       ///        by namespace when given multiple input pages.
-                       $result = $dbr->select(
-                               array( 'page', $table ),
-                               $fields,
-                               array_merge( $join,
-                                       array(
-                                               'page_namespace' => $title->getNamespace(),
-                                               'page_title' => $title->getDbKey() ) ),
-                               __METHOD__ );
-                       foreach( $result as $row ) {
-                               $template = Title::makeTitle( $row->namespace, $row->title );
-                               $pageSet[$template->getPrefixedText()] = true;
-                       }
-               }
-       }
-       return $pageSet;
-}
-
-/**
- * Callback function to remove empty strings from the pages array.
- */
-function wfFilterPage( $page ) {
-       return $page !== '' && $page !== null;
-}
-
-/**
- *
- */
-function wfSpecialExport( $page = '' ) {
-       global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors;
-       global $wgExportAllowHistory, $wgExportMaxHistory;
-
-       $curonly = true;
-       $doexport = false;
-
-       if ( $wgRequest->getCheck( 'addcat' ) ) {
-               $page = $wgRequest->getText( 'pages' );
-               $catname = $wgRequest->getText( 'catname' );
-
-               if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
-                       $t = Title::makeTitleSafe( NS_CATEGORY, $catname );
-                       if ( $t ) {
-                               /**
-                                * @fixme This can lead to hitting memory limit for very large
-                                * categories. Ideally we would do the lookup synchronously
-                                * during the export in a single query.
-                                */
-                               $catpages = wfExportGetPagesFromCategory( $t );
-                               if ( $catpages ) $page .= "\n" . implode( "\n", $catpages );
-                       }
-               }
-       }
-       else if( $wgRequest->wasPosted() && $page == '' ) {
-               $page = $wgRequest->getText( 'pages' );
-               $curonly = $wgRequest->getCheck( 'curonly' );
-               $rawOffset = $wgRequest->getVal( 'offset' );
-               if( $rawOffset ) {
-                       $offset = wfTimestamp( TS_MW, $rawOffset );
-               } else {
-                       $offset = null;
-               }
-               $limit = $wgRequest->getInt( 'limit' );
-               $dir = $wgRequest->getVal( 'dir' );
-               $history = array(
-                       'dir' => 'asc',
-                       'offset' => false,
-                       'limit' => $wgExportMaxHistory,
-               );
-               $historyCheck = $wgRequest->getCheck( 'history' );
-               if ( $curonly ) {
-                       $history = WikiExporter::CURRENT;
-               } elseif ( !$historyCheck ) {
-                       if ( $limit > 0 && $limit < $wgExportMaxHistory ) {
-                               $history['limit'] = $limit;
-                       }
-                       if ( !is_null( $offset ) ) {
-                               $history['offset'] = $offset;
-                       }
-                       if ( strtolower( $dir ) == 'desc' ) {
-                               $history['dir'] = 'desc';
-                       }
-               }
-
-               if( $page != '' ) $doexport = true;
-       } else {
-               // Default to current-only for GET requests
-               $page = $wgRequest->getText( 'pages', $page );
-               $historyCheck = $wgRequest->getCheck( 'history' );
-               if( $historyCheck ) {
-                       $history = WikiExporter::FULL;
-               } else {
-                       $history = WikiExporter::CURRENT;
-               }
-
-               if( $page != '' ) $doexport = true;
-       }
-
-       if( !$wgExportAllowHistory ) {
-               // Override
-               $history = WikiExporter::CURRENT;
-       }
-
-       $list_authors = $wgRequest->getCheck( 'listauthors' );
-       if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ;
-
-       if ( $doexport ) {
-               $wgOut->disable();
-
-               // Cancel output buffering and gzipping if set
-               // This should provide safer streaming for pages with history
-               wfResetOutputBuffers();
-               header( "Content-type: application/xml; charset=utf-8" );
-               if( $wgRequest->getCheck( 'wpDownload' ) ) {
-                       // Provide a sane filename suggestion
-                       $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
-                       $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
-               }
-
-               /* Split up the input and look up linked pages */
-               $inputPages = array_filter( explode( "\n", $page ), 'wfFilterPage' );
-               $pageSet = array_flip( $inputPages );
-
-               if( $wgRequest->getCheck( 'templates' ) ) {
-                       $pageSet = wfExportGetTemplates( $inputPages, $pageSet );
-               }
-
-               /*
-               // Enable this when we can do something useful exporting/importing image information. :)
-               if( $wgRequest->getCheck( 'images' ) ) {
-                       $pageSet = wfExportGetImages( $inputPages, $pageSet );
-               }
-               */
-
-               $pages = array_keys( $pageSet );
-
-               /* Ok, let's get to it... */
-
-               $db = wfGetDB( DB_SLAVE );
-               $exporter = new WikiExporter( $db, $history );
-               $exporter->list_authors = $list_authors ;
-               $exporter->openStream();
-
-               foreach( $pages as $page ) {
-                       /*
-                       if( $wgExportMaxHistory && !$curonly ) {
-                               $title = Title::newFromText( $page );
-                               if( $title ) {
-                                       $count = Revision::countByTitle( $db, $title );
-                                       if( $count > $wgExportMaxHistory ) {
-                                               wfDebug( __FUNCTION__ .
-                                                       ": Skipped $page, $count revisions too big\n" );
-                                               continue;
-                                       }
-                               }
-                       }*/
-
-                       #Bug 8824: Only export pages the user can read
-                       $title = Title::newFromText( $page );
-                       if( is_null( $title ) ) continue; #TODO: perhaps output an <error> tag or something.
-                       if( !$title->userCanRead() ) continue; #TODO: perhaps output an <error> tag or something.
-
-                       $exporter->pageByTitle( $title );
-               }
-
-               $exporter->closeStream();
-               return;
-       }
-
-       $self = SpecialPage::getTitleFor( 'Export' );
-       $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) );
-
-       $form = Xml::openElement( 'form', array( 'method' => 'post',
-               'action' => $self->getLocalUrl( 'action=submit' ) ) );
-
-       $form .= Xml::inputLabel( wfMsg( 'export-addcattext' )  , 'catname', 'catname', 40 ) . '&nbsp;';
-       $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
-
-       $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) );
-       $form .= htmlspecialchars( $page );
-       $form .= Xml::closeElement( 'textarea' );
-       $form .= '<br />';
-
-       if( $wgExportAllowHistory ) {
-               $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />';
-       } else {
-               $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) );
-       }
-       $form .= Xml::checkLabel( wfMsg( 'export-templates' ), 'templates', 'wpExportTemplates', false ) . '<br />';
-       // Enable this when we can do something useful exporting/importing image information. :)
-       //$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />';
-       $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />';
-
-       $form .= Xml::submitButton( wfMsg( 'export-submit' ) );
-       $form .= Xml::closeElement( 'form' );
-       $wgOut->addHtml( $form );
-}
diff --git a/includes/SpecialFewestrevisions.php b/includes/SpecialFewestrevisions.php
deleted file mode 100644 (file)
index 5ad8136..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Special page for listing the articles with the fewest revisions.
- *
- * @ingroup SpecialPage
- * @author Martin Drashkov
- */
-class FewestrevisionsPage extends QueryPage {
-
-       function getName() {
-               return 'Fewestrevisions';
-       }
-
-       function isExpensive() {
-               return true;
-       }
-
-       function isSyndicated() {
-               return false;
-       }
-
-       function getSql() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
-
-               return "SELECT 'Fewestrevisions' as type,
-                               page_namespace as namespace,
-                               page_title as title,
-                               COUNT(*) as value
-                       FROM $revision
-                       JOIN $page ON page_id = rev_page
-                       WHERE page_namespace = " . NS_MAIN . "
-                       GROUP BY 1,2,3
-                       HAVING COUNT(*) > 1";
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-
-               $nt = Title::makeTitleSafe( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getPrefixedText() );
-
-               $plink = $skin->makeKnownLinkObj( $nt, $text );
-
-               $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
-                       $wgLang->formatNum( $result->value ) );
-               $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
-
-               return wfSpecialList( $plink, $nlink );
-       }
-}
-
-function wfSpecialFewestrevisions() {
-       list( $limit, $offset ) = wfCheckLimits();
-       $frp = new FewestrevisionsPage();
-       $frp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialFileDuplicateSearch.php b/includes/SpecialFileDuplicateSearch.php
deleted file mode 100644 (file)
index 5236ca2..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-/**
- * A special page to search for files by hash value as defined in the
- * img_sha1 field in the image table
- *
- * @file
- * @ingroup SpecialPage
- *
- * @author Raimond Spekking, based on Special:MIMESearch by Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * Searches the database for files of the requested hash, comparing this with the
- * 'img_sha1' field in the image table.
- * @ingroup SpecialPage
- */
-class FileDuplicateSearchPage extends QueryPage {
-       var $hash, $filename;
-
-       function FileDuplicateSearchPage( $hash, $filename ) {
-               $this->hash = $hash;
-               $this->filename = $filename;
-       }
-
-       function getName() { return 'FileDuplicateSearch'; }
-       function isExpensive() { return false; }
-       function isSyndicated() { return false; }
-
-       function linkParameters() {
-               return array( 'filename' => $this->filename );
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $image = $dbr->tableName( 'image' );
-               $hash = $dbr->addQuotes( $this->hash );
-
-               return "SELECT 'FileDuplicateSearch' AS type,
-                               img_name AS title,
-                               img_sha1 AS value,
-                               img_user_text,
-                               img_timestamp
-                       FROM $image
-                       WHERE img_sha1 = $hash
-                       ";
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgContLang, $wgLang;
-
-               $nt = Title::makeTitle( NS_IMAGE, $result->title );
-               $text = $wgContLang->convert( $nt->getText() );
-               $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
-
-               $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
-               $time = $wgLang->timeanddate( $result->img_timestamp );
-
-               return "$plink . . $user . . $time";
-       }
-}
-
-/**
- * Output the HTML search form, and constructs the FileDuplicateSearch object.
- */
-function wfSpecialFileDuplicateSearch( $par = null ) {
-       global $wgRequest, $wgTitle, $wgOut, $wgLang, $wgContLang;
-
-       $hash = '';
-       $filename =  isset( $par ) ?  $par : $wgRequest->getText( 'filename' );
-
-       $title = Title::newFromText( $filename );
-       if( $title && $title->getText() != '' ) {
-               $dbr = wfGetDB( DB_SLAVE );
-               $image = $dbr->tableName( 'image' );
-               $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDbKey() ) );
-               $sql = "SELECT img_sha1 from $image where img_name = $encFilename";
-               $res = $dbr->query( $sql );
-               $row = $dbr->fetchRow( $res );
-               if( $row !== false ) {
-                       $hash = $row[0];
-               }
-               $dbr->freeResult( $res );
-       }
-
-       # Create the input form
-       $wgOut->addHTML(
-               Xml::openElement( 'form', array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
-               Xml::openElement( 'fieldset' ) .
-               Xml::element( 'legend', null, wfMsg( 'fileduplicatesearch-legend' ) ) .
-               Xml::inputLabel( wfMsg( 'fileduplicatesearch-filename' ), 'filename', 'filename', 50, $filename ) . ' ' .
-               Xml::submitButton( wfMsg( 'fileduplicatesearch-submit' ) ) .
-               Xml::closeElement( 'fieldset' ) .
-               Xml::closeElement( 'form' )
-       );
-
-       if( $hash != '' ) {
-               $align = $wgContLang->isRtl() ? 'left' : 'right';
-
-               # Show a thumbnail of the file
-               $img = wfFindFile( $title );
-               if ( $img ) {
-                       $thumb = $img->getThumbnail( 120, 120 );
-                       if( $thumb ) {
-                               $wgOut->addHTML( '<div style="float:' . $align . '" id="mw-fileduplicatesearch-icon">' .
-                                       $thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' .
-                                       wfMsgExt( 'fileduplicatesearch-info', array( 'parse' ),
-                                               $wgLang->formatNum( $img->getWidth() ),
-                                               $wgLang->formatNum( $img->getHeight() ),
-                                               $wgLang->formatSize( $img->getSize() ),
-                                               $img->getMimeType()
-                                       ) .
-                                       '</div>' );
-                       }
-               }
-
-               # Do the query
-               $wpp = new FileDuplicateSearchPage( $hash, $filename );
-               list( $limit, $offset ) = wfCheckLimits();
-               $count = $wpp->doQuery( $offset, $limit );
-
-               # Show a short summary
-               if( $count == 1 ) {
-                       $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-1">' .
-                               wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) .
-                               '</p>'
-                       );
-               } elseif ( $count > 1 ) {
-                       $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-n">' .
-                               wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) .
-                               '</p>'
-                       );
-               }
-       }
-}
diff --git a/includes/SpecialFilepath.php b/includes/SpecialFilepath.php
deleted file mode 100644 (file)
index a2ba3e5..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-function wfSpecialFilepath( $par ) {
-       global $wgRequest, $wgOut;
-
-       $file = isset( $par ) ? $par : $wgRequest->getText( 'file' );
-
-       $title = Title::newFromText( $file, NS_IMAGE );
-
-       if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) {
-               $cform = new FilepathForm( $title );
-               $cform->execute();
-       } else {
-               $file = wfFindFile( $title );
-               if ( $file && $file->exists() ) {
-                       $wgOut->redirect( $file->getURL() );
-               } else {
-                       $wgOut->setStatusCode( 404 );
-                       $cform = new FilepathForm( $title );
-                       $cform->execute();
-               }
-       }
-}
-
-/**
- * @ingroup SpecialPage
- */
-class FilepathForm {
-       var $mTitle;
-
-       function FilepathForm( &$title ) {
-               $this->mTitle =& $title;
-       }
-
-       function execute() {
-               global $wgOut, $wgTitle, $wgScript;
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'filepath' ) ) .
-                       Xml::hidden( 'title', $wgTitle->getPrefixedText() ) .
-                       Xml::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $this->mTitle ) ? $this->mTitle->getText() : '' ) . ' ' .
-                       Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' )
-               );
-       }
-}
diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php
deleted file mode 100644 (file)
index 3d449b5..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-function wfSpecialImagelist() {
-       global $wgOut;
-
-       $pager = new ImageListPager;
-
-       $limit = $pager->getForm();
-       $body = $pager->getBody();
-       $nav = $pager->getNavigationBar();
-       $wgOut->addHTML( "$limit<br />\n$body<br />\n$nav" );
-}
-
-/**
- * @ingroup SpecialPage Pager
- */
-class ImageListPager extends TablePager {
-       var $mFieldNames = null;
-       var $mQueryConds = array();
-
-       function __construct() {
-               global $wgRequest, $wgMiserMode;
-               if ( $wgRequest->getText( 'sort', 'img_date' ) == 'img_date' ) {
-                       $this->mDefaultDirection = true;
-               } else {
-                       $this->mDefaultDirection = false;
-               }
-               $search = $wgRequest->getText( 'ilsearch' );
-               if ( $search != '' && !$wgMiserMode ) {
-                       $nt = Title::newFromUrl( $search );
-                       if( $nt ) {
-                               $dbr = wfGetDB( DB_SLAVE );
-                               $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
-                               $m = str_replace( "%", "\\%", $m );
-                               $m = str_replace( "_", "\\_", $m );
-                               $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" );
-                       }
-               }
-
-               parent::__construct();
-       }
-
-       function getFieldNames() {
-               if ( !$this->mFieldNames ) {
-                       $this->mFieldNames = array(
-                               'img_timestamp' => wfMsg( 'imagelist_date' ),
-                               'img_name' => wfMsg( 'imagelist_name' ),
-                               'img_user_text' => wfMsg( 'imagelist_user' ),
-                               'img_size' => wfMsg( 'imagelist_size' ),
-                               'img_description' => wfMsg( 'imagelist_description' ),
-                       );
-               }
-               return $this->mFieldNames;
-       }
-
-       function isFieldSortable( $field ) {
-               static $sortable = array( 'img_timestamp', 'img_name', 'img_size' );
-               return in_array( $field, $sortable );
-       }
-
-       function getQueryInfo() {
-               $fields = $this->getFieldNames();
-               $fields = array_keys( $fields );
-               $fields[] = 'img_user';
-               return array(
-                       'tables' => 'image',
-                       'fields' => $fields,
-                       'conds' => $this->mQueryConds
-               );
-       }
-
-       function getDefaultSort() {
-               return 'img_timestamp';
-       }
-
-       function getStartBody() {
-               # Do a link batch query for user pages
-               if ( $this->mResult->numRows() ) {
-                       $lb = new LinkBatch;
-                       $this->mResult->seek( 0 );
-                       while ( $row = $this->mResult->fetchObject() ) {
-                               if ( $row->img_user ) {
-                                       $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) );
-                               }
-                       }
-                       $lb->execute();
-               }
-
-               return parent::getStartBody();
-       }
-
-       function formatValue( $field, $value ) {
-               global $wgLang;
-               switch ( $field ) {
-                       case 'img_timestamp':
-                               return $wgLang->timeanddate( $value, true );
-                       case 'img_name':
-                               static $imgfile = null;
-                               if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' );
-
-                               $name = $this->mCurrentRow->img_name;
-                               $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value );
-                               $image = wfLocalFile( $value );
-                               $url = $image->getURL();
-                               $download = Xml::element('a', array( 'href' => $url ), $imgfile );
-                               return "$link ($download)";
-                       case 'img_user_text':
-                               if ( $this->mCurrentRow->img_user ) {
-                                       $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ),
-                                               htmlspecialchars( $value ) );
-                               } else {
-                                       $link = htmlspecialchars( $value );
-                               }
-                               return $link;
-                       case 'img_size':
-                               return $this->getSkin()->formatSize( $value );
-                       case 'img_description':
-                               return $this->getSkin()->commentBlock( $value );
-               }
-       }
-
-       function getForm() {
-               global $wgRequest, $wgMiserMode;
-               $search = $wgRequest->getText( 'ilsearch' );
-
-               $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-imagelist-form' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'imagelist' ) ) .
-                       Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) );
-
-               if ( !$wgMiserMode ) {
-                       $s .= "<br />\n" .
-                               Xml::inputLabel( wfMsg( 'imagelist_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search );
-               }
-               $s .= ' ' .
-                       Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ."\n" .
-                       $this->getHiddenFields( array( 'limit', 'ilsearch' ) ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) . "\n";
-               return $s;
-       }
-
-       function getTableClass() {
-               return 'imagelist ' . parent::getTableClass();
-       }
-
-       function getNavClass() {
-               return 'imagelist_nav ' . parent::getNavClass();
-       }
-
-       function getSortHeaderClass() {
-               return 'imagelist_sort ' . parent::getSortHeaderClass();
-       }
-}
diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php
deleted file mode 100644 (file)
index 4c37f1f..0000000
+++ /dev/null
@@ -1,1154 +0,0 @@
-<?php
-/**
- * MediaWiki page data importer
- * Copyright (C) 2003,2005 Brion Vibber <brion@pobox.com>
- * http://www.mediawiki.org/
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor
- */
-function wfSpecialImport( $page = '' ) {
-       global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
-       global $wgImportTargetNamespace;
-
-       $interwiki = false;
-       $namespace = $wgImportTargetNamespace;
-       $frompage = '';
-       $history = true;
-
-       if ( wfReadOnly() ) {
-               $wgOut->readOnlyPage();
-               return;
-       }
-
-       if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
-               $isUpload = false;
-               $namespace = $wgRequest->getIntOrNull( 'namespace' );
-
-               switch( $wgRequest->getVal( "source" ) ) {
-               case "upload":
-                       $isUpload = true;
-                       if( $wgUser->isAllowed( 'importupload' ) ) {
-                               $source = ImportStreamSource::newFromUpload( "xmlimport" );
-                       } else {
-                               return $wgOut->permissionRequired( 'importupload' );
-                       }
-                       break;
-               case "interwiki":
-                       $interwiki = $wgRequest->getVal( 'interwiki' );
-                       $history = $wgRequest->getCheck( 'interwikiHistory' );
-                       $frompage = $wgRequest->getText( "frompage" );
-                       $source = ImportStreamSource::newFromInterwiki(
-                               $interwiki,
-                               $frompage,
-                               $history );
-                       break;
-               default:
-                       $source = new WikiErrorMsg( "importunknownsource" );
-               }
-
-               if( WikiError::isError( $source ) ) {
-                       $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $source->getMessage() ) );
-               } else {
-                       $wgOut->addWikiMsg( "importstart" );
-
-                       $importer = new WikiImporter( $source );
-                       if( !is_null( $namespace ) ) {
-                               $importer->setTargetNamespace( $namespace );
-                       }
-                       $reporter = new ImportReporter( $importer, $isUpload, $interwiki );
-
-                       $reporter->open();
-                       $result = $importer->doImport();
-                       $resultCount = $reporter->close();
-
-                       if( WikiError::isError( $result ) ) {
-                               # No source or XML parse error
-                               $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $result->getMessage() ) );
-                       } elseif( WikiError::isError( $resultCount ) ) {
-                               # Zero revisions
-                               $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $resultCount->getMessage() ) );
-                       } else {
-                               # Success!
-                               $wgOut->addWikiMsg( 'importsuccess' );
-                       }
-                       $wgOut->addWikiText( '<hr />' );
-               }
-       }
-
-       $action = $wgTitle->getLocalUrl( 'action=submit' );
-
-       if( $wgUser->isAllowed( 'importupload' ) ) {
-               $wgOut->addWikiMsg( "importtext" );
-               $wgOut->addHTML(
-                       Xml::openElement( 'fieldset' ).
-                       Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) .
-                       Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) .
-                       Xml::hidden( 'action', 'submit' ) .
-                       Xml::hidden( 'source', 'upload' ) .
-                       Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
-                       Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
-                       Xml::closeElement( 'form' ) .
-                       Xml::closeElement( 'fieldset' )
-               );
-       } else {
-               if( empty( $wgImportSources ) ) {
-                       $wgOut->addWikiMsg( 'importnosources' );
-               }
-       }
-
-       if( !empty( $wgImportSources ) ) {
-               $wgOut->addHTML(
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) .
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) .
-                       wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
-                       Xml::hidden( 'action', 'submit' ) .
-                       Xml::hidden( 'source', 'interwiki' ) .
-                       Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
-                       "<tr>
-                               <td>" .
-                                       Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
-               );
-               foreach( $wgImportSources as $prefix ) {
-                       $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : '';
-                       $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) );
-               }
-               $wgOut->addHTML(
-                                       Xml::closeElement( 'select' ) .
-                               "</td>
-                               <td>" .
-                                       Xml::input( 'frompage', 50, $frompage ) .
-                               "</td>
-                       </tr>
-                       <tr>
-                               <td>
-                               </td>
-                               <td>" .
-                                       Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) .
-                               "</td>
-                       </tr>
-                       <tr>
-                               <td>
-                               </td>
-                               <td>" .
-                                       Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
-                                       Xml::namespaceSelector( $namespace, '' ) .
-                               "</td>
-                       </tr>
-                       <tr>
-                               <td>
-                               </td>
-                               <td>" .
-                                       Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) .
-                               "</td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ).
-                       Xml::closeElement( 'form' ) .
-                       Xml::closeElement( 'fieldset' )
-               );
-       }
-}
-
-/**
- * Reporting callback
- * @ingroup SpecialPage
- */
-class ImportReporter {
-       function __construct( $importer, $upload, $interwiki ) {
-               $importer->setPageOutCallback( array( $this, 'reportPage' ) );
-               $this->mPageCount = 0;
-               $this->mIsUpload = $upload;
-               $this->mInterwiki = $interwiki;
-       }
-
-       function open() {
-               global $wgOut;
-               $wgOut->addHtml( "<ul>\n" );
-       }
-
-       function reportPage( $title, $origTitle, $revisionCount, $successCount ) {
-               global $wgOut, $wgUser, $wgLang, $wgContLang;
-
-               $skin = $wgUser->getSkin();
-
-               $this->mPageCount++;
-
-               $localCount = $wgLang->formatNum( $successCount );
-               $contentCount = $wgContLang->formatNum( $successCount );
-
-               if( $successCount > 0 ) {
-                       $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
-                               wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
-                               "</li>\n"
-                       );
-
-                       $log = new LogPage( 'import' );
-                       if( $this->mIsUpload ) {
-                               $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
-                                       $contentCount );
-                               $log->addEntry( 'upload', $title, $detail );
-                       } else {
-                               $interwiki = '[[:' . $this->mInterwiki . ':' .
-                                       $origTitle->getPrefixedText() . ']]';
-                               $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
-                                       $contentCount, $interwiki );
-                               $log->addEntry( 'interwiki', $title, $detail );
-                       }
-
-                       $comment = $detail; // quick
-                       $dbw = wfGetDB( DB_MASTER );
-                       $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true );
-                       $nullRevision->insertOn( $dbw );
-                       $article = new Article( $title );
-                       # Update page record
-                       $article->updateRevisionOn( $dbw, $nullRevision );
-                       wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
-               } else {
-                       $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
-               }
-       }
-
-       function close() {
-               global $wgOut;
-               if( $this->mPageCount == 0 ) {
-                       $wgOut->addHtml( "</ul>\n" );
-                       return new WikiErrorMsg( "importnopages" );
-               }
-               $wgOut->addHtml( "</ul>\n" );
-
-               return $this->mPageCount;
-       }
-}
-
-/**
- *
- * @ingroup SpecialPage
- */
-class WikiRevision {
-       var $title = null;
-       var $id = 0;
-       var $timestamp = "20010115000000";
-       var $user = 0;
-       var $user_text = "";
-       var $text = "";
-       var $comment = "";
-       var $minor = false;
-
-       function setTitle( $title ) {
-               if( is_object( $title ) ) {
-                       $this->title = $title;
-               } elseif( is_null( $title ) ) {
-                       throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." );
-               } else {
-                       throw new MWException( "WikiRevision given non-object title in import." );
-               }
-       }
-
-       function setID( $id ) {
-               $this->id = $id;
-       }
-
-       function setTimestamp( $ts ) {
-               # 2003-08-05T18:30:02Z
-               $this->timestamp = wfTimestamp( TS_MW, $ts );
-       }
-
-       function setUsername( $user ) {
-               $this->user_text = $user;
-       }
-
-       function setUserIP( $ip ) {
-               $this->user_text = $ip;
-       }
-
-       function setText( $text ) {
-               $this->text = $text;
-       }
-
-       function setComment( $text ) {
-               $this->comment = $text;
-       }
-
-       function setMinor( $minor ) {
-               $this->minor = (bool)$minor;
-       }
-
-       function setSrc( $src ) {
-               $this->src = $src;
-       }
-
-       function setFilename( $filename ) {
-               $this->filename = $filename;
-       }
-
-       function setSize( $size ) {
-               $this->size = intval( $size );
-       }
-
-       function getTitle() {
-               return $this->title;
-       }
-
-       function getID() {
-               return $this->id;
-       }
-
-       function getTimestamp() {
-               return $this->timestamp;
-       }
-
-       function getUser() {
-               return $this->user_text;
-       }
-
-       function getText() {
-               return $this->text;
-       }
-
-       function getComment() {
-               return $this->comment;
-       }
-
-       function getMinor() {
-               return $this->minor;
-       }
-
-       function getSrc() {
-               return $this->src;
-       }
-
-       function getFilename() {
-               return $this->filename;
-       }
-
-       function getSize() {
-               return $this->size;
-       }
-
-       function importOldRevision() {
-               $dbw = wfGetDB( DB_MASTER );
-
-               # Sneak a single revision into place
-               $user = User::newFromName( $this->getUser() );
-               if( $user ) {
-                       $userId = intval( $user->getId() );
-                       $userText = $user->getName();
-               } else {
-                       $userId = 0;
-                       $userText = $this->getUser();
-               }
-
-               // avoid memory leak...?
-               $linkCache = LinkCache::singleton();
-               $linkCache->clear();
-
-               $article = new Article( $this->title );
-               $pageId = $article->getId();
-               if( $pageId == 0 ) {
-                       # must create the page...
-                       $pageId = $article->insertOn( $dbw );
-                       $created = true;
-               } else {
-                       $created = false;
-
-                       $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp );
-                       if( !is_null( $prior ) ) {
-                               // FIXME: this could fail slightly for multiple matches :P
-                               wfDebug( __METHOD__ . ": skipping existing revision for [[" .
-                                       $this->title->getPrefixedText() . "]], timestamp " .
-                                       $this->timestamp . "\n" );
-                               return false;
-                       }
-               }
-
-               # FIXME: Use original rev_id optionally
-               # FIXME: blah blah blah
-
-               #if( $numrows > 0 ) {
-               #       return wfMsg( "importhistoryconflict" );
-               #}
-
-               # Insert the row
-               $revision = new Revision( array(
-                       'page'       => $pageId,
-                       'text'       => $this->getText(),
-                       'comment'    => $this->getComment(),
-                       'user'       => $userId,
-                       'user_text'  => $userText,
-                       'timestamp'  => $this->timestamp,
-                       'minor_edit' => $this->minor,
-                       ) );
-               $revId = $revision->insertOn( $dbw );
-               $changed = $article->updateIfNewerOn( $dbw, $revision );
-
-               if( $created ) {
-                       wfDebug( __METHOD__ . ": running onArticleCreate\n" );
-                       Article::onArticleCreate( $this->title );
-
-                       wfDebug( __METHOD__ . ": running create updates\n" );
-                       $article->createUpdates( $revision );
-
-               } elseif( $changed ) {
-                       wfDebug( __METHOD__ . ": running onArticleEdit\n" );
-                       Article::onArticleEdit( $this->title );
-
-                       wfDebug( __METHOD__ . ": running edit updates\n" );
-                       $article->editUpdates(
-                               $this->getText(),
-                               $this->getComment(),
-                               $this->minor,
-                               $this->timestamp,
-                               $revId );
-               }
-
-               return true;
-       }
-
-       function importUpload() {
-               wfDebug( __METHOD__ . ": STUB\n" );
-
-               /**
-                       // from file revert...
-                       $source = $this->file->getArchiveVirtualUrl( $this->oldimage );
-                       $comment = $wgRequest->getText( 'wpComment' );
-                       // TODO: Preserve file properties from database instead of reloading from file
-                       $status = $this->file->upload( $source, $comment, $comment );
-                       if( $status->isGood() ) {
-               */
-
-               /**
-                       // from file upload...
-               $this->mLocalFile = wfLocalFile( $nt );
-               $this->mDestName = $this->mLocalFile->getName();
-               //....
-                       $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
-                       File::DELETE_SOURCE, $this->mFileProps );
-                       if ( !$status->isGood() ) {
-                               $resultDetails = array( 'internal' => $status->getWikiText() );
-               */
-
-               // @fixme upload() uses $wgUser, which is wrong here
-               // it may also create a page without our desire, also wrong potentially.
-               // and, it will record a *current* upload, but we might want an archive version here
-
-               $file = wfLocalFile( $this->getTitle() );
-               if( !$file ) {
-                       var_dump( $file );
-                       wfDebug( "IMPORT: Bad file. :(\n" );
-                       return false;
-               }
-
-               $source = $this->downloadSource();
-               if( !$source ) {
-                       wfDebug( "IMPORT: Could not fetch remote file. :(\n" );
-                       return false;
-               }
-
-               $status = $file->upload( $source,
-                       $this->getComment(),
-                       $this->getComment(), // Initial page, if none present...
-                       File::DELETE_SOURCE,
-                       false, // props...
-                       $this->getTimestamp() );
-
-               if( $status->isGood() ) {
-                       // yay?
-                       wfDebug( "IMPORT: is ok?\n" );
-                       return true;
-               }
-
-               wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" );
-               return false;
-
-       }
-
-       function downloadSource() {
-               global $wgEnableUploads;
-               if( !$wgEnableUploads ) {
-                       return false;
-               }
-
-               $tempo = tempnam( wfTempDir(), 'download' );
-               $f = fopen( $tempo, 'wb' );
-               if( !$f ) {
-                       wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
-                       return false;
-               }
-
-               // @fixme!
-               $src = $this->getSrc();
-               $data = Http::get( $src );
-               if( !$data ) {
-                       wfDebug( "IMPORT: couldn't fetch source $src\n" );
-                       fclose( $f );
-                       unlink( $tempo );
-                       return false;
-               }
-
-               fwrite( $f, $data );
-               fclose( $f );
-
-               return $tempo;
-       }
-
-}
-
-/**
- * implements Special:Import
- * @ingroup SpecialPage
- */
-class WikiImporter {
-       var $mDebug = false;
-       var $mSource = null;
-       var $mPageCallback = null;
-       var $mPageOutCallback = null;
-       var $mRevisionCallback = null;
-       var $mUploadCallback = null;
-       var $mTargetNamespace = null;
-       var $lastfield;
-       var $tagStack = array();
-
-       function __construct( $source ) {
-               $this->setRevisionCallback( array( $this, "importRevision" ) );
-               $this->setUploadCallback( array( $this, "importUpload" ) );
-               $this->mSource = $source;
-       }
-
-       function throwXmlError( $err ) {
-               $this->debug( "FAILURE: $err" );
-               wfDebug( "WikiImporter XML error: $err\n" );
-       }
-
-       # --------------
-
-       function doImport() {
-               if( empty( $this->mSource ) ) {
-                       return new WikiErrorMsg( "importnotext" );
-               }
-
-               $parser = xml_parser_create( "UTF-8" );
-
-               # case folding violates XML standard, turn it off
-               xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
-
-               xml_set_object( $parser, $this );
-               xml_set_element_handler( $parser, "in_start", "" );
-
-               $offset = 0; // for context extraction on error reporting
-               do {
-                       $chunk = $this->mSource->readChunk();
-                       if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
-                               wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
-                               return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset );
-                       }
-                       $offset += strlen( $chunk );
-               } while( $chunk !== false && !$this->mSource->atEnd() );
-               xml_parser_free( $parser );
-
-               return true;
-       }
-
-       function debug( $data ) {
-               if( $this->mDebug ) {
-                       wfDebug( "IMPORT: $data\n" );
-               }
-       }
-
-       function notice( $data ) {
-               global $wgCommandLineMode;
-               if( $wgCommandLineMode ) {
-                       print "$data\n";
-               } else {
-                       global $wgOut;
-                       $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" );
-               }
-       }
-
-       /**
-        * Set debug mode...
-        */
-       function setDebug( $debug ) {
-               $this->mDebug = $debug;
-       }
-
-       /**
-        * Sets the action to perform as each new page in the stream is reached.
-        * @param $callback callback
-        * @return callback
-        */
-       function setPageCallback( $callback ) {
-               $previous = $this->mPageCallback;
-               $this->mPageCallback = $callback;
-               return $previous;
-       }
-
-       /**
-        * Sets the action to perform as each page in the stream is completed.
-        * Callback accepts the page title (as a Title object), a second object
-        * with the original title form (in case it's been overridden into a
-        * local namespace), and a count of revisions.
-        *
-        * @param $callback callback
-        * @return callback
-        */
-       function setPageOutCallback( $callback ) {
-               $previous = $this->mPageOutCallback;
-               $this->mPageOutCallback = $callback;
-               return $previous;
-       }
-
-       /**
-        * Sets the action to perform as each page revision is reached.
-        * @param $callback callback
-        * @return callback
-        */
-       function setRevisionCallback( $callback ) {
-               $previous = $this->mRevisionCallback;
-               $this->mRevisionCallback = $callback;
-               return $previous;
-       }
-
-       /**
-        * Sets the action to perform as each file upload version is reached.
-        * @param $callback callback
-        * @return callback
-        */
-       function setUploadCallback( $callback ) {
-               $previous = $this->mUploadCallback;
-               $this->mUploadCallback = $callback;
-               return $previous;
-       }
-
-       /**
-        * Set a target namespace to override the defaults
-        */
-       function setTargetNamespace( $namespace ) {
-               if( is_null( $namespace ) ) {
-                       // Don't override namespaces
-                       $this->mTargetNamespace = null;
-               } elseif( $namespace >= 0 ) {
-                       // FIXME: Check for validity
-                       $this->mTargetNamespace = intval( $namespace );
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * Default per-revision callback, performs the import.
-        * @param $revision WikiRevision
-        * @private
-        */
-       function importRevision( $revision ) {
-               $dbw = wfGetDB( DB_MASTER );
-               return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
-       }
-
-       /**
-        * Dummy for now...
-        */
-       function importUpload( $revision ) {
-               //$dbw = wfGetDB( DB_MASTER );
-               //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) );
-               return false;
-       }
-
-       /**
-        * Alternate per-revision callback, for debugging.
-        * @param $revision WikiRevision
-        * @private
-        */
-       function debugRevisionHandler( &$revision ) {
-               $this->debug( "Got revision:" );
-               if( is_object( $revision->title ) ) {
-                       $this->debug( "-- Title: " . $revision->title->getPrefixedText() );
-               } else {
-                       $this->debug( "-- Title: <invalid>" );
-               }
-               $this->debug( "-- User: " . $revision->user_text );
-               $this->debug( "-- Timestamp: " . $revision->timestamp );
-               $this->debug( "-- Comment: " . $revision->comment );
-               $this->debug( "-- Text: " . $revision->text );
-       }
-
-       /**
-        * Notify the callback function when a new <page> is reached.
-        * @param $title Title
-        * @private
-        */
-       function pageCallback( $title ) {
-               if( is_callable( $this->mPageCallback ) ) {
-                       call_user_func( $this->mPageCallback, $title );
-               }
-       }
-
-       /**
-        * Notify the callback function when a </page> is closed.
-        * @param $title Title
-        * @param $origTitle Title
-        * @param $revisionCount int
-        * @param $successCount Int: number of revisions for which callback returned true
-        * @private
-        */
-       function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) {
-               if( is_callable( $this->mPageOutCallback ) ) {
-                       call_user_func( $this->mPageOutCallback, $title, $origTitle,
-                               $revisionCount, $successCount );
-               }
-       }
-
-
-       # XML parser callbacks from here out -- beware!
-       function donothing( $parser, $x, $y="" ) {
-               #$this->debug( "donothing" );
-       }
-
-       function in_start( $parser, $name, $attribs ) {
-               $this->debug( "in_start $name" );
-               if( $name != "mediawiki" ) {
-                       return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
-               }
-               xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
-       }
-
-       function in_mediawiki( $parser, $name, $attribs ) {
-               $this->debug( "in_mediawiki $name" );
-               if( $name == 'siteinfo' ) {
-                       xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
-               } elseif( $name == 'page' ) {
-                       $this->push( $name );
-                       $this->workRevisionCount = 0;
-                       $this->workSuccessCount = 0;
-                       $this->uploadCount = 0;
-                       $this->uploadSuccessCount = 0;
-                       xml_set_element_handler( $parser, "in_page", "out_page" );
-               } else {
-                       return $this->throwXMLerror( "Expected <page>, got <$name>" );
-               }
-       }
-       function out_mediawiki( $parser, $name ) {
-               $this->debug( "out_mediawiki $name" );
-               if( $name != "mediawiki" ) {
-                       return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
-               }
-               xml_set_element_handler( $parser, "donothing", "donothing" );
-       }
-
-
-       function in_siteinfo( $parser, $name, $attribs ) {
-               // no-ops for now
-               $this->debug( "in_siteinfo $name" );
-               switch( $name ) {
-               case "sitename":
-               case "base":
-               case "generator":
-               case "case":
-               case "namespaces":
-               case "namespace":
-                       break;
-               default:
-                       return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." );
-               }
-       }
-
-       function out_siteinfo( $parser, $name ) {
-               if( $name == "siteinfo" ) {
-                       xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
-               }
-       }
-
-
-       function in_page( $parser, $name, $attribs ) {
-               $this->debug( "in_page $name" );
-               switch( $name ) {
-               case "id":
-               case "title":
-               case "restrictions":
-                       $this->appendfield = $name;
-                       $this->appenddata = "";
-                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
-                       xml_set_character_data_handler( $parser, "char_append" );
-                       break;
-               case "revision":
-                       $this->push( "revision" );
-                       if( is_object( $this->pageTitle ) ) {
-                               $this->workRevision = new WikiRevision;
-                               $this->workRevision->setTitle( $this->pageTitle );
-                               $this->workRevisionCount++;
-                       } else {
-                               // Skipping items due to invalid page title
-                               $this->workRevision = null;
-                       }
-                       xml_set_element_handler( $parser, "in_revision", "out_revision" );
-                       break;
-               case "upload":
-                       $this->push( "upload" );
-                       if( is_object( $this->pageTitle ) ) {
-                               $this->workRevision = new WikiRevision;
-                               $this->workRevision->setTitle( $this->pageTitle );
-                               $this->uploadCount++;
-                       } else {
-                               // Skipping items due to invalid page title
-                               $this->workRevision = null;
-                       }
-                       xml_set_element_handler( $parser, "in_upload", "out_upload" );
-                       break;
-               default:
-                       return $this->throwXMLerror( "Element <$name> not allowed in a <page>." );
-               }
-       }
-
-       function out_page( $parser, $name ) {
-               $this->debug( "out_page $name" );
-               $this->pop();
-               if( $name != "page" ) {
-                       return $this->throwXMLerror( "Expected </page>, got </$name>" );
-               }
-               xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
-
-               $this->pageOutCallback( $this->pageTitle, $this->origTitle,
-                       $this->workRevisionCount, $this->workSuccessCount );
-
-               $this->workTitle = null;
-               $this->workRevision = null;
-               $this->workRevisionCount = 0;
-               $this->workSuccessCount = 0;
-               $this->pageTitle = null;
-               $this->origTitle = null;
-       }
-
-       function in_nothing( $parser, $name, $attribs ) {
-               $this->debug( "in_nothing $name" );
-               return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
-       }
-       function char_append( $parser, $data ) {
-               $this->debug( "char_append '$data'" );
-               $this->appenddata .= $data;
-       }
-       function out_append( $parser, $name ) {
-               $this->debug( "out_append $name" );
-               if( $name != $this->appendfield ) {
-                       return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
-               }
-
-               switch( $this->appendfield ) {
-               case "title":
-                       $this->workTitle = $this->appenddata;
-                       $this->origTitle = Title::newFromText( $this->workTitle );
-                       if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
-                               $this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
-                                       $this->origTitle->getDBkey() );
-                       } else {
-                               $this->pageTitle = Title::newFromText( $this->workTitle );
-                       }
-                       if( is_null( $this->pageTitle ) ) {
-                               // Invalid page title? Ignore the page
-                               $this->notice( "Skipping invalid page title '$this->workTitle'" );
-                       } else {
-                               $this->pageCallback( $this->workTitle );
-                       }
-                       break;
-               case "id":
-                       if ( $this->parentTag() == 'revision' ) {
-                               if( $this->workRevision )
-                                       $this->workRevision->setID( $this->appenddata );
-                       }
-                       break;
-               case "text":
-                       if( $this->workRevision )
-                               $this->workRevision->setText( $this->appenddata );
-                       break;
-               case "username":
-                       if( $this->workRevision )
-                               $this->workRevision->setUsername( $this->appenddata );
-                       break;
-               case "ip":
-                       if( $this->workRevision )
-                               $this->workRevision->setUserIP( $this->appenddata );
-                       break;
-               case "timestamp":
-                       if( $this->workRevision )
-                               $this->workRevision->setTimestamp( $this->appenddata );
-                       break;
-               case "comment":
-                       if( $this->workRevision )
-                               $this->workRevision->setComment( $this->appenddata );
-                       break;
-               case "minor":
-                       if( $this->workRevision )
-                               $this->workRevision->setMinor( true );
-                       break;
-               case "filename":
-                       if( $this->workRevision )
-                               $this->workRevision->setFilename( $this->appenddata );
-                       break;
-               case "src":
-                       if( $this->workRevision )
-                               $this->workRevision->setSrc( $this->appenddata );
-                       break;
-               case "size":
-                       if( $this->workRevision )
-                               $this->workRevision->setSize( intval( $this->appenddata ) );
-                       break;
-               default:
-                       $this->debug( "Bad append: {$this->appendfield}" );
-               }
-               $this->appendfield = "";
-               $this->appenddata = "";
-
-               $parent = $this->parentTag();
-               xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
-               xml_set_character_data_handler( $parser, "donothing" );
-       }
-
-       function in_revision( $parser, $name, $attribs ) {
-               $this->debug( "in_revision $name" );
-               switch( $name ) {
-               case "id":
-               case "timestamp":
-               case "comment":
-               case "minor":
-               case "text":
-                       $this->appendfield = $name;
-                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
-                       xml_set_character_data_handler( $parser, "char_append" );
-                       break;
-               case "contributor":
-                       $this->push( "contributor" );
-                       xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
-                       break;
-               default:
-                       return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
-               }
-       }
-
-       function out_revision( $parser, $name ) {
-               $this->debug( "out_revision $name" );
-               $this->pop();
-               if( $name != "revision" ) {
-                       return $this->throwXMLerror( "Expected </revision>, got </$name>" );
-               }
-               xml_set_element_handler( $parser, "in_page", "out_page" );
-
-               if( $this->workRevision ) {
-                       $ok = call_user_func_array( $this->mRevisionCallback,
-                               array( $this->workRevision, $this ) );
-                       if( $ok ) {
-                               $this->workSuccessCount++;
-                       }
-               }
-       }
-
-       function in_upload( $parser, $name, $attribs ) {
-               $this->debug( "in_upload $name" );
-               switch( $name ) {
-               case "timestamp":
-               case "comment":
-               case "text":
-               case "filename":
-               case "src":
-               case "size":
-                       $this->appendfield = $name;
-                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
-                       xml_set_character_data_handler( $parser, "char_append" );
-                       break;
-               case "contributor":
-                       $this->push( "contributor" );
-                       xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
-                       break;
-               default:
-                       return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." );
-               }
-       }
-
-       function out_upload( $parser, $name ) {
-               $this->debug( "out_revision $name" );
-               $this->pop();
-               if( $name != "upload" ) {
-                       return $this->throwXMLerror( "Expected </upload>, got </$name>" );
-               }
-               xml_set_element_handler( $parser, "in_page", "out_page" );
-
-               if( $this->workRevision ) {
-                       $ok = call_user_func_array( $this->mUploadCallback,
-                               array( $this->workRevision, $this ) );
-                       if( $ok ) {
-                               $this->workUploadSuccessCount++;
-                       }
-               }
-       }
-
-       function in_contributor( $parser, $name, $attribs ) {
-               $this->debug( "in_contributor $name" );
-               switch( $name ) {
-               case "username":
-               case "ip":
-               case "id":
-                       $this->appendfield = $name;
-                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
-                       xml_set_character_data_handler( $parser, "char_append" );
-                       break;
-               default:
-                       $this->throwXMLerror( "Invalid tag <$name> in <contributor>" );
-               }
-       }
-
-       function out_contributor( $parser, $name ) {
-               $this->debug( "out_contributor $name" );
-               $this->pop();
-               if( $name != "contributor" ) {
-                       return $this->throwXMLerror( "Expected </contributor>, got </$name>" );
-               }
-               $parent = $this->parentTag();
-               xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
-       }
-
-       private function push( $name ) {
-               array_push( $this->tagStack, $name );
-               $this->debug( "PUSH $name" );
-       }
-
-       private function pop() {
-               $name = array_pop( $this->tagStack );
-               $this->debug( "POP $name" );
-               return $name;
-       }
-
-       private function parentTag() {
-               $name = $this->tagStack[count( $this->tagStack ) - 1];
-               $this->debug( "PARENT $name" );
-               return $name;
-       }
-
-}
-
-/**
- * @todo document (e.g. one-sentence class description).
- * @ingroup SpecialPage
- */
-class ImportStringSource {
-       function __construct( $string ) {
-               $this->mString = $string;
-               $this->mRead = false;
-       }
-
-       function atEnd() {
-               return $this->mRead;
-       }
-
-       function readChunk() {
-               if( $this->atEnd() ) {
-                       return false;
-               } else {
-                       $this->mRead = true;
-                       return $this->mString;
-               }
-       }
-}
-
-/**
- * @todo document (e.g. one-sentence class description).
- * @ingroup SpecialPage
- */
-class ImportStreamSource {
-       function __construct( $handle ) {
-               $this->mHandle = $handle;
-       }
-
-       function atEnd() {
-               return feof( $this->mHandle );
-       }
-
-       function readChunk() {
-               return fread( $this->mHandle, 32768 );
-       }
-
-       static function newFromFile( $filename ) {
-               $file = @fopen( $filename, 'rt' );
-               if( !$file ) {
-                       return new WikiErrorMsg( "importcantopen" );
-               }
-               return new ImportStreamSource( $file );
-       }
-
-       static function newFromUpload( $fieldname = "xmlimport" ) {
-               $upload =& $_FILES[$fieldname];
-
-               if( !isset( $upload ) || !$upload['name'] ) {
-                       return new WikiErrorMsg( 'importnofile' );
-               }
-               if( !empty( $upload['error'] ) ) {
-                       switch($upload['error']){
-                               case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
-                                       return new WikiErrorMsg( 'importuploaderrorsize' );
-                               case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
-                                       return new WikiErrorMsg( 'importuploaderrorsize' );
-                               case 3: # The uploaded file was only partially uploaded
-                                       return new WikiErrorMsg( 'importuploaderrorpartial' );
-                           case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
-                               return new WikiErrorMsg( 'importuploaderrortemp' );
-                           # case else: # Currently impossible
-                       }
-
-               }
-               $fname = $upload['tmp_name'];
-               if( is_uploaded_file( $fname ) ) {
-                       return ImportStreamSource::newFromFile( $fname );
-               } else {
-                       return new WikiErrorMsg( 'importnofile' );
-               }
-       }
-
-       static function newFromURL( $url, $method = 'GET' ) {
-               wfDebug( __METHOD__ . ": opening $url\n" );
-               # Use the standard HTTP fetch function; it times out
-               # quicker and sorts out user-agent problems which might
-               # otherwise prevent importing from large sites, such
-               # as the Wikimedia cluster, etc.
-               $data = Http::request( $method, $url );
-               if( $data !== false ) {
-                       $file = tmpfile();
-                       fwrite( $file, $data );
-                       fflush( $file );
-                       fseek( $file, 0 );
-                       return new ImportStreamSource( $file );
-               } else {
-                       return new WikiErrorMsg( 'importcantopen' );
-               }
-       }
-
-       public static function newFromInterwiki( $interwiki, $page, $history=false ) {
-               if( $page == '' ) {
-                       return new WikiErrorMsg( 'import-noarticle' );
-               }
-               $link = Title::newFromText( "$interwiki:Special:Export/$page" );
-               if( is_null( $link ) || $link->getInterwiki() == '' ) {
-                       return new WikiErrorMsg( 'importbadinterwiki' );
-               } else {
-                       $params = $history ? 'history=1' : '';
-                       $url = $link->getFullUrl( $params );
-                       # For interwikis, use POST to avoid redirects.
-                       return ImportStreamSource::newFromURL( $url, "POST" );
-               }
-       }
-}
diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php
deleted file mode 100644 (file)
index 696c7ef..0000000
+++ /dev/null
@@ -1,427 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @todo document
- */
-function wfSpecialIpblocklist() {
-       global $wgUser, $wgOut, $wgRequest;
-
-       $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) );
-       $id = $wgRequest->getVal( 'id' );
-       $reason = $wgRequest->getText( 'wpUnblockReason' );
-       $action = $wgRequest->getText( 'action' );
-       $successip = $wgRequest->getVal( 'successip' );
-
-       $ipu = new IPUnblockForm( $ip, $id, $reason );
-
-       if( $action == 'unblock' ) {
-               # Check permissions
-               if( !$wgUser->isAllowed( 'block' ) ) {
-                       $wgOut->permissionRequired( 'block' );
-                       return;
-               }
-               # Check for database lock
-               if( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-               # Show unblock form
-               $ipu->showForm( '' );
-       } elseif( $action == 'submit' && $wgRequest->wasPosted()
-               && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
-               # Check permissions
-               if( !$wgUser->isAllowed( 'block' ) ) {
-                       $wgOut->permissionRequired( 'block' );
-                       return;
-               }
-               # Check for database lock
-               if( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-               # Remove blocks and redirect user to success page
-               $ipu->doSubmit();
-       } elseif( $action == 'success' ) {
-               # Inform the user of a successful unblock
-               # (No need to check permissions or locks here,
-               # if something was done, then it's too late!)
-               if ( substr( $successip, 0, 1) == '#' ) {
-                       // A block ID was unblocked
-                       $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) );
-               } else {
-                       // A username/IP was unblocked
-                       $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) );
-               }
-       } else {
-               # Just show the block list
-               $ipu->showList( '' );
-       }
-
-}
-
-/**
- * implements Special:ipblocklist GUI
- * @ingroup SpecialPage
- */
-class IPUnblockForm {
-       var $ip, $reason, $id;
-
-       function IPUnblockForm( $ip, $id, $reason ) {
-               $this->ip = strtr( $ip, '_', ' ' );
-               $this->id = $id;
-               $this->reason = $reason;
-       }
-
-       /**
-        * Generates the unblock form
-        * @param $err string: error message
-        * @return $out string: HTML form
-        */
-       function showForm( $err ) {
-               global $wgOut, $wgUser, $wgSysopUserBans;
-
-               $wgOut->setPagetitle( wfMsg( 'unblockip' ) );
-               $wgOut->addWikiMsg( 'unblockiptext' );
-
-               $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
-               $action = $titleObj->getLocalURL( "action=submit" );
-
-               if ( "" != $err ) {
-                       $wgOut->setSubtitle( wfMsg( "formerror" ) );
-                       $wgOut->addWikiText( Xml::tags( 'span', array( 'class' => 'error' ), $err ) . "\n" );
-               }
-
-               $addressPart = false;
-               if ( $this->id ) {
-                       $block = Block::newFromID( $this->id );
-                       if ( $block ) {
-                               $encName = htmlspecialchars( $block->getRedactedName() );
-                               $encId = $this->id;
-                               $addressPart = $encName . Xml::hidden( 'id', $encId );
-                               $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' );
-                       }
-               }
-               if ( !$addressPart ) {
-                       $addressPart = Xml::input( 'wpUnblockAddress', 40, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) );
-                       $ipa = Xml::label( wfMsg( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ), 'wpUnblockAddress' );
-               }
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'ipb-unblock' ) ) .
-                       Xml::openElement( 'table', array( 'id' => 'mw-unblock-table' ) ).
-                       "<tr>
-                               <td class='mw-label'>
-                                       {$ipa}
-                               </td>
-                               <td class='mw-input'>
-                                       {$addressPart}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'ipbreason' ), 'wpUnblockReason' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                                       Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) .
-                               "</td>
-                       </tr>
-                       <tr>
-                               <td>&nbsp;</td>
-                               <td class='mw-submit'>" .
-                                       Xml::submitButton( wfMsg( 'ipusubmit' ), array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) .
-                               "</td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-
-       }
-
-       const UNBLOCK_SUCCESS = 0; // Success
-       const UNBLOCK_NO_SUCH_ID = 1; // No such block ID
-       const UNBLOCK_USER_NOT_BLOCKED = 2; // IP wasn't blocked
-       const UNBLOCK_BLOCKED_AS_RANGE = 3; // IP is part of a range block
-       const UNBLOCK_UNKNOWNERR = 4; // Unknown error
-
-       /**
-        * Backend code for unblocking. doSubmit() wraps around this.
-        * $range is only used when UNBLOCK_BLOCKED_AS_RANGE is returned, in which
-        * case it contains the range $ip is part of.
-        * @return array array(message key, parameters) on failure, empty array on success
-        */
-
-       static function doUnblock(&$id, &$ip, &$reason, &$range = null)
-       {
-               if ( $id ) {
-                       $block = Block::newFromID( $id );
-                       if ( !$block ) {
-                               return array('ipb_cant_unblock', htmlspecialchars($id));
-                       }
-                       $ip = $block->getRedactedName();
-               } else {
-                       $block = new Block();
-                       $ip = trim( $ip );
-                       if ( substr( $ip, 0, 1 ) == "#" ) {
-                               $id = substr( $ip, 1 );
-                               $block = Block::newFromID( $id );
-                               if( !$block ) {
-                                       return array('ipb_cant_unblock', htmlspecialchars($id));
-                               }
-                               $ip = $block->getRedactedName();
-                       } else {
-                               $block = Block::newFromDB( $ip );
-                               if ( !$block ) {
-                                       return array('ipb_cant_unblock', htmlspecialchars($id));
-                               }
-                               if( $block->mRangeStart != $block->mRangeEnd
-                                               && !strstr( $ip, "/" ) ) {
-                                       /* If the specified IP is a single address, and the block is
-                                        * a range block, don't unblock the range. */
-                                        $range = $block->mAddress;
-                                        return array('ipb_blocked_as_range', $ip, $range);
-                               }
-                       }
-               }
-               // Yes, this is really necessary
-               $id = $block->mId;
-
-               # Delete block
-               if ( !$block->delete() ) {
-                       return array('ipb_cant_unblock', htmlspecialchars($id));
-               }
-
-               # Make log entry
-               $log = new LogPage( 'block' );
-               $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $ip ), $reason );
-               return array();
-       }
-
-       function doSubmit() {
-               global $wgOut;
-               $retval = self::doUnblock($this->id, $this->ip, $this->reason, $range);
-               if(!empty($retval))
-               {
-                       $key = array_shift($retval);
-                       $this->showForm(wfMsgReal($key, $retval));
-                       return;
-               }
-               # Report to the user
-               $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
-               $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) );
-               $wgOut->redirect( $success );
-       }
-
-       function showList( $msg ) {
-               global $wgOut, $wgUser;
-
-               $wgOut->setPagetitle( wfMsg( "ipblocklist" ) );
-               if ( "" != $msg ) {
-                       $wgOut->setSubtitle( $msg );
-               }
-
-               // Purge expired entries on one in every 10 queries
-               if ( !mt_rand( 0, 10 ) ) {
-                       Block::purgeExpired();
-               }
-
-               $conds = array();
-               $matches = array();
-               // Is user allowed to see all the blocks?
-               if ( !$wgUser->isAllowed( 'suppress' ) )
-                       $conds['ipb_deleted'] = 0;
-               if ( $this->ip == '' ) {
-                       // No extra conditions
-               } elseif ( substr( $this->ip, 0, 1 ) == '#' ) {
-                       $conds['ipb_id'] = substr( $this->ip, 1 );
-               } elseif ( IP::toUnsigned( $this->ip ) !== false ) {
-                       $conds['ipb_address'] = $this->ip;
-                       $conds['ipb_auto'] = 0;
-               } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) {
-                       $conds['ipb_address'] = Block::normaliseRange( $this->ip );
-                       $conds['ipb_auto'] = 0;
-               } else {
-                       $user = User::newFromName( $this->ip );
-                       if ( $user && ( $id = $user->getId() ) != 0 ) {
-                               $conds['ipb_user'] = $id;
-                       } else {
-                               // Uh...?
-                               $conds['ipb_address'] = $this->ip;
-                               $conds['ipb_auto'] = 0;
-                       }
-               }
-
-               $pager = new IPBlocklistPager( $this, $conds );
-               if ( $pager->getNumRows() ) {
-                       $wgOut->addHTML(
-                               $this->searchForm() .
-                               $pager->getNavigationBar() .
-                               Xml::tags( 'ul', null, $pager->getBody() ) .
-                               $pager->getNavigationBar()
-                       );
-               } elseif ( $this->ip != '') {
-                       $wgOut->addHTML( $this->searchForm() );
-                       $wgOut->addWikiMsg( 'ipblocklist-no-results' );
-               } else {
-                       $wgOut->addWikiMsg( 'ipblocklist-empty' );
-               }
-       }
-
-       function searchForm() {
-               global $wgTitle, $wgScript, $wgRequest;
-               return
-                       Xml::tags( 'form', array( 'action' => $wgScript ),
-                               Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
-                               Xml::openElement( 'fieldset' ) .
-                               Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) .
-                               Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) .
-                               '&nbsp;' .
-                               Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) .
-                               Xml::closeElement( 'fieldset' )
-                       );
-       }
-
-       /**
-        * Callback function to output a block
-        */
-       function formatRow( $block ) {
-               global $wgUser, $wgLang;
-
-               wfProfileIn( __METHOD__ );
-
-               static $sk=null, $msg=null;
-
-               if( is_null( $sk ) )
-                       $sk = $wgUser->getSkin();
-               if( is_null( $msg ) ) {
-                       $msg = array();
-                       $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink',
-                               'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
-                       foreach( $keys as $key ) {
-                               $msg[$key] = wfMsgHtml( $key );
-                       }
-                       $msg['blocklistline'] = wfMsg( 'blocklistline' );
-               }
-
-               # Prepare links to the blocker's user and talk pages
-               $blocker_id = $block->getBy();
-               $blocker_name = $block->getByName();
-               $blocker = $sk->userLink( $blocker_id, $blocker_name );
-               $blocker .= $sk->userToolLinks( $blocker_id, $blocker_name );
-
-               # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks)
-               if( $block->mAuto ) {
-                       $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy
-               } else {
-                       $target = $sk->userLink( $block->mUser, $block->mAddress )
-                               . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK );
-               }
-
-               $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
-
-               $properties = array();
-               $properties[] = Block::formatExpiry( $block->mExpiry );
-               if ( $block->mAnonOnly ) {
-                       $properties[] = $msg['anononlyblock'];
-               }
-               if ( $block->mCreateAccount ) {
-                       $properties[] = $msg['createaccountblock'];
-               }
-               if (!$block->mEnableAutoblock && $block->mUser ) {
-                       $properties[] = $msg['noautoblockblock'];
-               }
-
-               if ( $block->mBlockEmail && $block->mUser ) {
-                       $properties[] = $msg['emailblock'];
-               }
-
-               $properties = implode( ', ', $properties );
-
-               $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) );
-
-               $unblocklink = '';
-               if ( $wgUser->isAllowed('block') ) {
-                       $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
-                       $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')';
-               }
-
-               $comment = $sk->commentBlock( $block->mReason );
-
-               $s = "{$line} $comment";
-               if ( $block->mHideName )
-                       $s = '<span class="history-deleted">' . $s . '</span>';
-
-               wfProfileOut( __METHOD__ );
-               return "<li>$s $unblocklink</li>\n";
-       }
-}
-
-/**
- * @todo document
- * @ingroup Pager
- */
-class IPBlocklistPager extends ReverseChronologicalPager {
-       public $mForm, $mConds;
-
-       function __construct( $form, $conds = array() ) {
-               $this->mForm = $form;
-               $this->mConds = $conds;
-               parent::__construct();
-       }
-
-       function getStartBody() {
-               wfProfileIn( __METHOD__ );
-               # Do a link batch query
-               $this->mResult->seek( 0 );
-               $lb = new LinkBatch;
-
-               /*
-               while ( $row = $this->mResult->fetchObject() ) {
-                       $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
-                       $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
-                       $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) );
-                       $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) );
-               }*/
-               # Faster way
-               # Usernames and titles are in fact related by a simple substitution of space -> underscore
-               # The last few lines of Title::secureAndSplit() tell the story.
-               while ( $row = $this->mResult->fetchObject() ) {
-                       $name = str_replace( ' ', '_', $row->ipb_by_text );
-                       $lb->add( NS_USER, $name );
-                       $lb->add( NS_USER_TALK, $name );
-                       $name = str_replace( ' ', '_', $row->ipb_address );
-                       $lb->add( NS_USER, $name );
-                       $lb->add( NS_USER_TALK, $name );
-               }
-               $lb->execute();
-               wfProfileOut( __METHOD__ );
-               return '';
-       }
-
-       function formatRow( $row ) {
-               $block = new Block;
-               $block->initFromRow( $row );
-               return $this->mForm->formatRow( $block );
-       }
-
-       function getQueryInfo() {
-               $conds = $this->mConds;
-               $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
-               return array(
-                       'tables' => 'ipblocks',
-                       'fields' => '*',
-                       'conds' => $conds,
-               );
-       }
-
-       function getIndexField() {
-               return 'ipb_timestamp';
-       }
-}
diff --git a/includes/SpecialListgrouprights.php b/includes/SpecialListgrouprights.php
deleted file mode 100644 (file)
index 131c060..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-/**
- * This special page lists all defined user groups and the associated rights.
- * See also @ref $wgGroupPermissions.
- *
- * @ingroup SpecialPage
- * @author Petr Kadlec <mormegil@centrum.cz>
- */
-class SpecialListGroupRights extends SpecialPage {
-
-       var $skin;
-
-       /**
-        * Constructor
-        */
-       function __construct() {
-               global $wgUser;
-               parent::__construct( 'Listgrouprights' );
-               $this->skin = $wgUser->getSkin();
-       }
-
-       /**
-        * Show the special page
-        */
-       public function execute( $par ) {
-               global $wgOut, $wgGroupPermissions, $wgImplicitGroups, $wgMessageCache;
-               $wgMessageCache->loadAllMessages();
-
-               $this->setHeaders();
-               $this->outputHeader();
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'table', array( 'class' => 'mw-listgrouprights-table' ) ) .
-                               '<tr>' .
-                                       Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) .
-                                       Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) .
-                               '</tr>'
-               );
-
-               foreach( $wgGroupPermissions as $group => $permissions ) {
-                       $groupname = ( $group == '*' ) ? 'all' : htmlspecialchars( $group ); // Replace * with a more descriptive groupname
-
-                       $msg = wfMsg( 'group-' . $groupname );
-                       if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) {
-                               $groupnameLocalized = $groupname;
-                       } else {
-                               $groupnameLocalized = $msg;
-                       }
-
-                       $msg = wfMsgForContent( 'grouppage-' . $groupname );
-                       if ( wfEmptyMsg( 'grouppage-' . $groupname, $msg ) || $msg == '' ) {
-                               $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
-                       } else {
-                               $grouppageLocalized = $msg;
-                       }
-
-                       if( $group == '*' ) {
-                               // Do not make a link for the generic * group
-                               $grouppage = $groupnameLocalized;
-                       } else {
-                               $grouppage = $this->skin->makeLink( $grouppageLocalized, $groupnameLocalized );
-                       }
-
-                       if ( !in_array( $group, $wgImplicitGroups ) ) {
-                               $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group );
-                       } else {
-                               // No link to Special:listusers for implicit groups as they are unlistable
-                               $grouplink = '';
-                       }
-
-                       $wgOut->addHTML(
-                               '<tr>
-                                       <td>' .
-                                               $grouppage . $grouplink .
-                                       '</td>
-                                       <td>' .
-                                               self::formatPermissions( $permissions ) .
-                                       '</td>
-                               </tr>'
-                       );
-               }
-               $wgOut->addHTML(
-                       Xml::closeElement( 'table' ) . "\n"
-               );
-       }
-
-       /**
-        * Create a user-readable list of permissions from the given array.
-        *
-        * @param $permissions Array of permission => bool (from $wgGroupPermissions items)
-        * @return string List of all granted permissions, separated by comma separator
-        */
-        private static function formatPermissions( $permissions ) {
-               $r = array();
-               foreach( $permissions as $permission => $granted ) {
-                       if ( $granted ) {
-                               $description = wfMsgHTML( 'listgrouprights-right-display',
-                                       User::getRightDescription($permission),
-                                       $permission
-                               );
-                               $r[] = $description;
-                       }
-               }
-               sort( $r );
-               if( empty( $r ) ) {
-                       return '';
-               } else {
-                       return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
-               }
-       }
-}
diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php
deleted file mode 100644 (file)
index 808aab1..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- *
- * @author Rob Church <robchur@gmail.com>
- * @copyright Â© 2006 Rob Church
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * Special:Listredirects - Lists all the redirects on the wiki.
- * @ingroup SpecialPage
- */
-class ListredirectsPage extends QueryPage {
-
-       function getName() { return( 'Listredirects' ); }
-       function isExpensive() { return( true ); }
-       function isSyndicated() { return( false ); }
-       function sortDescending() { return( false ); }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $page = $dbr->tableName( 'page' );
-               $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page WHERE page_is_redirect = 1";
-               return( $sql );
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgContLang;
-
-               # Make a link to the redirect itself
-               $rd_title = Title::makeTitle( $result->namespace, $result->title );
-               $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' );
-
-               # Find out where the redirect leads
-               $revision = Revision::newFromTitle( $rd_title );
-               if( $revision ) {
-                       # Make a link to the destination page
-                       $target = Title::newFromRedirect( $revision->getText() );
-                       if( $target ) {
-                               $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
-                               $targetLink = $skin->makeLinkObj( $target );
-                               return "$rd_link $arr $targetLink";
-                       } else {
-                               return "<s>$rd_link</s>";
-                       }
-               } else {
-                       return "<s>$rd_link</s>";
-               }
-       }
-}
-
-function wfSpecialListredirects() {
-       list( $limit, $offset ) = wfCheckLimits();
-       $lrp = new ListredirectsPage();
-       $lrp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialListusers.php b/includes/SpecialListusers.php
deleted file mode 100644 (file)
index 7dba44e..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-<?php
-
-# Copyright (C) 2004 Brion Vibber, lcrocker, Tim Starling,
-# Domas Mituzas, Ashar Voultoiz, Jens Frank, Zhengzhu.
-#
-# Â© 2006 Rob Church <robchur@gmail.com>
-#
-# http://www.mediawiki.org/
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# http://www.gnu.org/copyleft/gpl.html
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * This class is used to get a list of user. The ones with specials
- * rights (sysop, bureaucrat, developer) will have them displayed
- * next to their names.
- *
- * @ingroup SpecialPage
- */
-class UsersPager extends AlphabeticPager {
-
-       function __construct($group=null) {
-               global $wgRequest;
-               $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' );
-               $un = $wgRequest->getText( 'username' );
-               $this->requestedUser = '';
-               if ( $un != '' ) {
-                       $username = Title::makeTitleSafe( NS_USER, $un );
-                       if( ! is_null( $username ) ) {
-                               $this->requestedUser = $username->getText();
-                       }
-               }
-               parent::__construct();
-       }
-
-
-       function getIndexField() {
-               return 'user_name';
-       }
-
-       function getQueryInfo() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $conds=array();
-               // don't show hidden names
-               $conds[]='ipb_deleted IS NULL OR ipb_deleted = 0';
-               if ($this->requestedGroup != "") {
-                       $conds['ug_group'] = $this->requestedGroup;
-                       $useIndex = '';
-               } else {
-                       $useIndex = $dbr->useIndexClause('user_name');
-               }
-               if ($this->requestedUser != "") {
-                       $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
-               }
-
-               list ($user,$user_groups,$ipblocks) = $dbr->tableNamesN('user','user_groups','ipblocks');
-
-               $query = array(
-                       'tables' => " $user $useIndex LEFT JOIN $user_groups ON user_id=ug_user
-                               LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ",
-                       'fields' => array('user_name',
-                               'MAX(user_id) AS user_id',
-                               'COUNT(ug_group) AS numgroups',
-                               'MAX(ug_group) AS singlegroup'),
-                       'options' => array('GROUP BY' => 'user_name'),
-                       'conds' => $conds
-               );
-
-               wfRunHooks( 'SpecialListusersQueryInfo', array( $this, &$query ) );
-               return $query;
-       }
-
-       function formatRow( $row ) {
-               $userPage = Title::makeTitle( NS_USER, $row->user_name );
-               $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
-
-               if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) {
-                       $list = array();
-                       foreach( self::getGroups( $row->user_id ) as $group )
-                               $list[] = self::buildGroupLink( $group );
-                       $groups = implode( ', ', $list );
-               } elseif( $row->numgroups == 1 ) {
-                       $groups = self::buildGroupLink( $row->singlegroup );
-               } else {
-                       $groups = '';
-               }
-
-               $item = wfSpecialList( $name, $groups );
-               wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
-               return "<li>{$item}</li>";
-       }
-
-       function getBody() {
-               if (!$this->mQueryDone) {
-                       $this->doQuery();
-               }
-               $batch = new LinkBatch;
-
-               $this->mResult->rewind();
-
-               while ( $row = $this->mResult->fetchObject() ) {
-                       $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
-               }
-               $batch->execute();
-               $this->mResult->rewind();
-               return parent::getBody();
-       }
-
-       function getPageHeader( ) {
-               global $wgScript, $wgRequest;
-               $self = $this->getTitle();
-
-               # Form tag
-               $out  = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
-                       '<fieldset>' .
-                       Xml::element( 'legend', array(), wfMsg( 'listusers' ) );
-               $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() );
-
-               # Username field
-               $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' .
-                       Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
-
-               # Group drop-down list
-               $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' .
-                       Xml::openElement('select',  array( 'name' => 'group', 'id' => 'group' ) ) .
-                       Xml::option( wfMsg( 'group-all' ), '' );
-               foreach( $this->getAllGroups() as $group => $groupText )
-                       $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
-               $out .= Xml::closeElement( 'select' ) . ' ';
-
-               wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
-
-               # Submit button and form bottom
-               if( $this->mLimit )
-                       $out .= Xml::hidden( 'limit', $this->mLimit );
-               $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
-               wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) );
-               $out .= '</fieldset>' .
-                       Xml::closeElement( 'form' );
-
-               return $out;
-       }
-
-       function getAllGroups() {
-               $result = array();
-               foreach( User::getAllGroups() as $group ) {
-                       $result[$group] = User::getGroupName( $group );
-               }
-               return $result;
-       }
-
-       /**
-        * Preserve group and username offset parameters when paging
-        * @return array
-        */
-       function getDefaultQuery() {
-               $query = parent::getDefaultQuery();
-               if( $this->requestedGroup != '' )
-                       $query['group'] = $this->requestedGroup;
-               if( $this->requestedUser != '' )
-                       $query['username'] = $this->requestedUser;
-               wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) );
-               return $query;
-       }
-
-       /**
-        * Get a list of groups the specified user belongs to
-        *
-        * @param int $uid
-        * @return array
-        */
-       protected static function getGroups( $uid ) {
-               $dbr = wfGetDB( DB_SLAVE );
-               $groups = array();
-               $res = $dbr->select( 'user_groups', 'ug_group', array( 'ug_user' => $uid ), __METHOD__ );
-               if( $res && $dbr->numRows( $res ) > 0 ) {
-                       while( $row = $dbr->fetchObject( $res ) )
-                               $groups[] = $row->ug_group;
-                       $dbr->freeResult( $res );
-               }
-               return $groups;
-       }
-
-       /**
-        * Format a link to a group description page
-        *
-        * @param string $group
-        * @return string
-        */
-       protected static function buildGroupLink( $group ) {
-               static $cache = array();
-               if( !isset( $cache[$group] ) )
-                       $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
-               return $cache[$group];
-       }
-}
-
-/**
- * constructor
- * $par string (optional) A group to list users from
- */
-function wfSpecialListusers( $par = null ) {
-       global $wgRequest, $wgOut;
-
-       $up = new UsersPager($par);
-
-       # getBody() first to check, if empty
-       $usersbody = $up->getBody();
-       $s = $up->getPageHeader();
-       if( $usersbody ) {
-               $s .=   $up->getNavigationBar();
-               $s .=   '<ul>' . $usersbody . '</ul>';
-               $s .=   $up->getNavigationBar() ;
-       } else {
-               $s .=   '<p>' . wfMsgHTML('listusers-noresult') . '</p>';
-       };
-
-       $wgOut->addHTML( $s );
-}
diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php
deleted file mode 100644 (file)
index 0401922..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor
- */
-function wfSpecialLockdb() {
-       global $wgUser, $wgOut, $wgRequest;
-
-       if( !$wgUser->isAllowed( 'siteadmin' ) ) {
-               $wgOut->permissionRequired( 'siteadmin' );
-               return;
-       }
-
-       # If the lock file isn't writable, we can do sweet bugger all
-       global $wgReadOnlyFile;
-       if( !is_writable( dirname( $wgReadOnlyFile ) ) ) {
-               DBLockForm::notWritable();
-               return;
-       }
-
-       $action = $wgRequest->getVal( 'action' );
-       $f = new DBLockForm();
-
-       if ( 'success' == $action ) {
-               $f->showSuccess();
-       } else if ( 'submit' == $action && $wgRequest->wasPosted() &&
-               $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
-               $f->doSubmit();
-       } else {
-               $f->showForm( '' );
-       }
-}
-
-/**
- * A form to make the database readonly (eg for maintenance purposes).
- * @ingroup SpecialPage
- */
-class DBLockForm {
-       var $reason = '';
-
-       function DBLockForm() {
-               global $wgRequest;
-               $this->reason = $wgRequest->getText( 'wpLockReason' );
-       }
-
-       function showForm( $err ) {
-               global $wgOut, $wgUser;
-
-               $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
-               $wgOut->addWikiMsg( 'lockdbtext' );
-
-               if ( "" != $err ) {
-                       $wgOut->setSubtitle( wfMsg( 'formerror' ) );
-                       $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
-               }
-               $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) );
-               $lb = htmlspecialchars( wfMsg( 'lockbtn' ) );
-               $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) );
-               $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
-               $action = $titleObj->escapeLocalURL( 'action=submit' );
-               $reason = htmlspecialchars( $this->reason );
-               $token = htmlspecialchars( $wgUser->editToken() );
-
-               $wgOut->addHTML( <<<END
-<form id="lockdb" method="post" action="{$action}">
-{$elr}:
-<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea>
-<table border="0">
-       <tr>
-               <td align="right">
-                       <input type="checkbox" name="wpLockConfirm" />
-               </td>
-               <td align="left">{$lc}</td>
-       </tr>
-       <tr>
-               <td>&nbsp;</td>
-               <td align="left">
-                       <input type="submit" name="wpLock" value="{$lb}" />
-               </td>
-       </tr>
-</table>
-<input type="hidden" name="wpEditToken" value="{$token}" />
-</form>
-END
-);
-
-       }
-
-       function doSubmit() {
-               global $wgOut, $wgUser, $wgLang, $wgRequest;
-               global $wgReadOnlyFile;
-
-               if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
-                       $this->showForm( wfMsg( 'locknoconfirm' ) );
-                       return;
-               }
-               $fp = @fopen( $wgReadOnlyFile, 'w' );
-
-               if ( false === $fp ) {
-                       # This used to show a file not found error, but the likeliest reason for fopen()
-                       # to fail at this point is insufficient permission to write to the file...good old
-                       # is_writable() is plain wrong in some cases, it seems...
-                       self::notWritable();
-                       return;
-               }
-               fwrite( $fp, $this->reason );
-               fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " .
-                 $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
-               fclose( $fp );
-
-               $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
-               $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
-       }
-
-       function showSuccess() {
-               global $wgOut;
-
-               $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
-               $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
-               $wgOut->addWikiMsg( 'lockdbsuccesstext' );
-       }
-
-       public static function notWritable() {
-               global $wgOut;
-               $wgOut->showErrorPage( 'lockdb', 'lockfilenotwritable' );
-       }
-}
diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php
deleted file mode 100644 (file)
index 3154ed1..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-# Copyright (C) 2008 Aaron Schulz
-# http://www.mediawiki.org/
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# http://www.gnu.org/copyleft/gpl.html
-
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * constructor
- */
-function wfSpecialLog( $par = '' ) {
-       global $wgRequest, $wgOut, $wgUser;
-       # Get parameters
-       $type = $wgRequest->getVal( 'type', $par );
-       $user = $wgRequest->getText( 'user' );
-       $title = $wgRequest->getText( 'page' );
-       $pattern = $wgRequest->getBool( 'pattern' );
-       $y = $wgRequest->getIntOrNull( 'year' );
-       $m = $wgRequest->getIntOrNull( 'month' );
-       # Don't let the user get stuck with a certain date
-       $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
-       if( $skip ) {
-               $y = '';
-               $m = '';
-       }
-       # Create a LogPager item to get the results and a LogEventsList
-       # item to format them...
-       $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
-       $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m );
-       # Set title and add header
-       $loglist->showHeader( $pager->getType() );
-       # Show form options
-       $loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
-               $pager->getYear(), $pager->getMonth() );
-       # Insert list
-       $logBody = $pager->getBody();
-       if( $logBody ) {
-               $wgOut->addHTML(
-                       $pager->getNavigationBar() .
-                       $loglist->beginLogEventsList() .
-                       $logBody .
-                       $loglist->endLogEventsList() .
-                       $pager->getNavigationBar()
-               );
-       } else {
-               $wgOut->addWikiMsg( 'logempty' );
-       }
-}
diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php
deleted file mode 100644 (file)
index 5aafac7..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page looking for articles with no article linking to them,
- * thus being lonely.
- * @ingroup SpecialPage
- */
-class LonelyPagesPage extends PageQueryPage {
-
-       function getName() {
-               return "Lonelypages";
-       }
-       function getPageHeader() {
-               return wfMsgExt( 'lonelypagestext', array( 'parse' ) );
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function isExpensive() {
-               return true;
-       }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
-
-               return
-                 "SELECT 'Lonelypages'  AS type,
-                         page_namespace AS namespace,
-                         page_title     AS title,
-                         page_title     AS value
-                    FROM $page
-               LEFT JOIN $pagelinks
-                      ON page_namespace=pl_namespace AND page_title=pl_title
-                   WHERE pl_namespace IS NULL
-                     AND page_namespace=".NS_MAIN."
-                     AND page_is_redirect=0";
-
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialLonelypages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $lpp = new LonelyPagesPage();
-
-       return $lpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialLongpages.php b/includes/SpecialLongpages.php
deleted file mode 100644 (file)
index be16a02..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- * @ingroup SpecialPage
- */
-class LongPagesPage extends ShortPagesPage {
-
-       function getName() {
-               return "Longpages";
-       }
-
-       function sortDescending() {
-               return true;
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialLongpages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $lpp = new LongPagesPage();
-
-       $lpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php
deleted file mode 100644 (file)
index 82ee4be..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-/**
- * A special page to search for files by MIME type as defined in the
- * img_major_mime and img_minor_mime fields in the image table
- *
- * @file
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * Searches the database for files of the requested MIME type, comparing this with the
- * 'img_major_mime' and 'img_minor_mime' fields in the image table.
- * @ingroup SpecialPage
- */
-class MIMEsearchPage extends QueryPage {
-       var $major, $minor;
-
-       function MIMEsearchPage( $major, $minor ) {
-               $this->major = $major;
-               $this->minor = $minor;
-       }
-
-       function getName() { return 'MIMEsearch'; }
-
-       /**
-        * Due to this page relying upon extra fields being passed in the SELECT it
-        * will fail if it's set as expensive and misermode is on
-        */
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       function linkParameters() {
-               $arr = array( $this->major, $this->minor );
-               $mime = implode( '/', $arr );
-               return array( 'mime' => $mime );
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $image = $dbr->tableName( 'image' );
-               $major = $dbr->addQuotes( $this->major );
-               $minor = $dbr->addQuotes( $this->minor );
-
-               return
-                       "SELECT 'MIMEsearch' AS type,
-                               " . NS_IMAGE . " AS namespace,
-                               img_name AS title,
-                               img_major_mime AS value,
-
-                               img_size,
-                               img_width,
-                               img_height,
-                               img_user_text,
-                               img_timestamp
-                       FROM $image
-                       WHERE img_major_mime = $major AND img_minor_mime = $minor
-                       ";
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgContLang, $wgLang;
-
-               $nt = Title::makeTitle( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getText() );
-               $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
-
-               $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
-               $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
-                       $wgLang->formatNum( $result->img_size ) );
-               $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ),
-                       $wgLang->formatNum( $result->img_height ) );
-               $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
-               $time = $wgLang->timeanddate( $result->img_timestamp );
-
-               return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
-       }
-}
-
-/**
- * Output the HTML search form, and constructs the MIMEsearchPage object.
- */
-function wfSpecialMIMEsearch( $par = null ) {
-       global $wgRequest, $wgTitle, $wgOut;
-
-       $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
-
-       $wgOut->addHTML(
-               Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
-               Xml::openElement( 'fieldset' ) .
-               Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
-               Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
-               Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
-               Xml::closeElement( 'fieldset' ) .
-               Xml::closeElement( 'form' )
-       );
-
-       list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
-       if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
-               return;
-       $wpp = new MIMEsearchPage( $major, $minor );
-
-       list( $limit, $offset ) = wfCheckLimits();
-       $wpp->doQuery( $offset, $limit );
-}
-
-function wfSpecialMIMEsearchParse( $str ) {
-       // searched for an invalid MIME type.
-       if( strpos( $str, '/' ) === false) {
-               return array ('', '');
-       }
-
-       list( $major, $minor ) = explode( '/', $str, 2 );
-
-       return array(
-               ltrim( $major, ' ' ),
-               rtrim( $minor, ' ' )
-       );
-}
-
-function wfSpecialMIMEsearchValidType( $type ) {
-       // From maintenance/tables.sql => img_major_mime
-       $types = array(
-               'unknown',
-               'application',
-               'audio',
-               'image',
-               'text',
-               'video',
-               'message',
-               'model',
-               'multipart'
-       );
-
-       return in_array( $type, $types );
-}
diff --git a/includes/SpecialMergeHistory.php b/includes/SpecialMergeHistory.php
deleted file mode 100644 (file)
index 6183374..0000000
+++ /dev/null
@@ -1,451 +0,0 @@
-<?php
-/**
- * Special page allowing users with the appropriate permissions to
- * merge article histories, with some restrictions
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor
- */
-function wfSpecialMergehistory( $par ) {
-       global $wgRequest;
-
-       $form = new MergehistoryForm( $wgRequest, $par );
-       $form->execute();
-}
-
-/**
- * The HTML form for Special:MergeHistory, which allows users with the appropriate
- * permissions to view and restore deleted content.
- * @ingroup SpecialPage
- */
-class MergehistoryForm {
-       var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
-       var $mTargetObj, $mDestObj;
-
-       function MergehistoryForm( $request, $par = "" ) {
-               global $wgUser;
-
-               $this->mAction = $request->getVal( 'action' );
-               $this->mTarget = $request->getVal( 'target' );
-               $this->mDest = $request->getVal( 'dest' );
-               $this->mSubmitted = $request->getBool( 'submitted' );
-
-               $this->mTargetID = intval( $request->getVal( 'targetID' ) );
-               $this->mDestID = intval( $request->getVal( 'destID' ) );
-               $this->mTimestamp = $request->getVal( 'mergepoint' );
-               if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
-                       $this->mTimestamp = '';
-               }
-               $this->mComment = $request->getText( 'wpComment' );
-
-               $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
-               // target page
-               if( $this->mSubmitted ) {
-                       $this->mTargetObj = Title::newFromURL( $this->mTarget );
-                       $this->mDestObj = Title::newFromURL( $this->mDest );
-               } else {
-                       $this->mTargetObj = null;
-                       $this->mDestObj = null;
-               }
-
-               $this->preCacheMessages();
-       }
-
-       /**
-        * As we use the same small set of messages in various methods and that
-        * they are called often, we call them once and save them in $this->message
-        */
-       function preCacheMessages() {
-               // Precache various messages
-               if( !isset( $this->message ) ) {
-                       $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
-               }
-       }
-
-       function execute() {
-               global $wgOut, $wgUser;
-
-               $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
-
-               if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
-                       return $this->merge();
-               }
-
-               if ( !$this->mSubmitted ) {
-                       $this->showMergeForm();
-                       return;
-               }
-
-               $errors = array();
-               if ( !$this->mTargetObj instanceof Title ) {
-                       $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
-               } elseif( !$this->mTargetObj->exists() ) {
-                       $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
-                               wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
-                       );
-               }
-
-               if ( !$this->mDestObj instanceof Title) {
-                       $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
-               } elseif( !$this->mDestObj->exists() ) {
-                       $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
-                               wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
-                       );
-               }
-
-               // TODO: warn about target = dest?
-
-               if ( count( $errors ) ) {
-                       $this->showMergeForm();
-                       $wgOut->addHTML( implode( "\n", $errors ) );
-               } else {
-                       $this->showHistory();
-               }
-
-       }
-
-       function showMergeForm() {
-               global $wgOut, $wgScript;
-
-               $wgOut->addWikiMsg( 'mergehistory-header' );
-
-               $wgOut->addHtml(
-                       Xml::openElement( 'form', array(
-                               'method' => 'get',
-                               'action' => $wgScript ) ) .
-                       '<fieldset>' .
-                       Xml::element( 'legend', array(),
-                               wfMsg( 'mergehistory-box' ) ) .
-                       Xml::hidden( 'title',
-                               SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .
-                       Xml::hidden( 'submitted', '1' ) .
-                       Xml::hidden( 'mergepoint', $this->mTimestamp ) .
-                       Xml::openElement( 'table' ) .
-                       "<tr>
-                               <td>".Xml::label( wfMsg( 'mergehistory-from' ), 'target' )."</td>
-                               <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>
-                       </tr><tr>
-                               <td>".Xml::label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>
-                               <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>
-                       </tr><tr><td>" .
-                       Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .
-                       "</td></tr>" .
-                       Xml::closeElement( 'table' ) .
-                       '</fieldset>' .
-                       '</form>' );
-       }
-
-       private function showHistory() {
-               global $wgLang, $wgContLang, $wgUser, $wgOut;
-
-               $this->sk = $wgUser->getSkin();
-
-               $wgOut->setPagetitle( wfMsg( "mergehistory" ) );
-
-               $this->showMergeForm();
-
-               # List all stored revisions
-               $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );
-               $haveRevisions = $revisions && $revisions->getNumRows() > 0;
-
-               $titleObj = SpecialPage::getTitleFor( "Mergehistory" );
-               $action = $titleObj->getLocalURL( "action=submit" );
-               # Start the form here
-               $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
-               $wgOut->addHtml( $top );
-
-               if( $haveRevisions ) {
-                       # Format the user-visible controls (comment field, submission button)
-                       # in a nice little table
-                       $align = $wgContLang->isRtl() ? 'left' : 'right';
-                       $table =
-                               Xml::openElement( 'fieldset' ) .
-                               Xml::openElement( 'table' ) .
-                                       "<tr>
-                                               <td colspan='2'>" .
-                                                       wfMsgExt( 'mergehistory-merge', array('parseinline'),
-                                                               $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
-                                               "</td>
-                                       </tr>
-                                       <tr>
-                                               <td align='$align'>" .
-                                                       Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
-                                               "</td>
-                                               <td>" .
-                                                       Xml::input( 'wpComment', 50, $this->mComment ) .
-                                               "</td>
-                                       </tr>
-                                       <tr>
-                                               <td>&nbsp;</td>
-                                               <td>" .
-                                                       Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
-                                               "</td>
-                                       </tr>" .
-                               Xml::closeElement( 'table' ) .
-                               Xml::closeElement( 'fieldset' );
-
-                       $wgOut->addHtml( $table );
-               }
-
-               $wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
-
-               if( $haveRevisions ) {
-                       $wgOut->addHTML( $revisions->getNavigationBar() );
-                       $wgOut->addHTML( "<ul>" );
-                       $wgOut->addHTML( $revisions->getBody() );
-                       $wgOut->addHTML( "</ul>" );
-                       $wgOut->addHTML( $revisions->getNavigationBar() );
-               } else {
-                       $wgOut->addWikiMsg( "mergehistory-empty" );
-               }
-
-               # Show relevant lines from the deletion log:
-               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
-               LogEventsList::showLogExtract( $wgOut, 'merge', $this->mTargetObj->getPrefixedText() );
-
-               # When we submit, go by page ID to avoid some nasty but unlikely collisions.
-               # Such would happen if a page was renamed after the form loaded, but before submit
-               $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );
-               $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );
-               $misc .= Xml::hidden( 'target', $this->mTarget );
-               $misc .= Xml::hidden( 'dest', $this->mDest );
-               $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
-               $misc .= Xml::closeElement( 'form' );
-               $wgOut->addHtml( $misc );
-
-               return true;
-       }
-
-       function formatRevisionRow( $row ) {
-               global $wgUser, $wgLang;
-
-               $rev = new Revision( $row );
-
-               $stxt = '';
-               $last = $this->message['last'];
-
-               $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
-               $checkBox = wfRadio( "mergepoint", $ts, false );
-
-               $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
-                       htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() );
-               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
-                       $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
-               }
-
-               # Last link
-               if( !$rev->userCan( Revision::DELETED_TEXT ) )
-                       $last = $this->message['last'];
-               else if( isset($this->prevId[$row->rev_id]) )
-                       $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'],
-                               "diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] );
-
-               $userLink = $this->sk->revUserTools( $rev );
-
-               if(!is_null($size = $row->rev_len)) {
-                       if($size == 0)
-                               $stxt = wfMsgHtml('historyempty');
-                       else
-                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
-               }
-               $comment = $this->sk->revComment( $rev );
-
-               return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
-       }
-
-       /**
-        * Fetch revision text link if it's available to all users
-        * @return string
-        */
-       function getPageLink( $row, $titleObj, $ts, $target ) {
-               global $wgLang;
-
-               if( !$this->userCan($row, Revision::DELETED_TEXT) ) {
-                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
-               } else {
-                       $link = $this->sk->makeKnownLinkObj( $titleObj,
-                               $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
-                       if( $this->isDeleted($row, Revision::DELETED_TEXT) )
-                               $link = '<span class="history-deleted">' . $link . '</span>';
-                       return $link;
-               }
-       }
-
-       function merge() {
-               global $wgOut, $wgUser;
-               # Get the titles directly from the IDs, in case the target page params
-               # were spoofed. The queries are done based on the IDs, so it's best to
-               # keep it consistent...
-               $targetTitle = Title::newFromID( $this->mTargetID );
-               $destTitle = Title::newFromID( $this->mDestID );
-               if( is_null($targetTitle) || is_null($destTitle) )
-                       return false; // validate these
-               if( $targetTitle->getArticleId() == $destTitle->getArticleId() )
-                       return false;
-               # Verify that this timestamp is valid
-               # Must be older than the destination page
-               $dbw = wfGetDB( DB_MASTER );
-               # Get timestamp into DB format
-               $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp($this->mTimestamp) : '';
-               # Max timestamp should be min of destination page
-               $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',
-                       array('rev_page' => $this->mDestID ),
-                       __METHOD__ );
-               # Destination page must exist with revisions
-               if( !$maxtimestamp ) {
-                       $wgOut->addWikiMsg('mergehistory-fail');
-                       return false;
-               }
-               # Get the latest timestamp of the source
-               $lasttimestamp = $dbw->selectField( array('page','revision'),
-                       'rev_timestamp',
-                       array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),
-                       __METHOD__ );
-               # $this->mTimestamp must be older than $maxtimestamp
-               if( $this->mTimestamp >= $maxtimestamp ) {
-                       $wgOut->addWikiMsg('mergehistory-fail');
-                       return false;
-               }
-               # Update the revisions
-               if( $this->mTimestamp ) {
-                       $timewhere = "rev_timestamp <= {$this->mTimestamp}";
-                       $TimestampLimit = wfTimestamp(TS_MW,$this->mTimestamp);
-               } else {
-                       $timewhere = "rev_timestamp <= {$maxtimestamp}";
-                       $TimestampLimit = wfTimestamp(TS_MW,$lasttimestamp);
-               }
-               # Do the moving...
-               $dbw->update( 'revision',
-                       array( 'rev_page' => $this->mDestID ),
-                       array( 'rev_page' => $this->mTargetID,
-                               $timewhere ),
-                       __METHOD__ );
-
-               $count = $dbw->affectedRows();
-               # Make the source page a redirect if no revisions are left
-               $haveRevisions = $dbw->selectField( 'revision',
-                       'rev_timestamp',
-                       array( 'rev_page' => $this->mTargetID  ),
-                       __METHOD__,
-                       array( 'FOR UPDATE' ) );
-               if( !$haveRevisions ) {
-                       if( $this->mComment ) {
-                               $comment = wfMsgForContent( 'mergehistory-comment', $targetTitle->getPrefixedText(),
-                                       $destTitle->getPrefixedText(), $this->mComment );
-                       } else {
-                               $comment = wfMsgForContent( 'mergehistory-autocomment', $targetTitle->getPrefixedText(),
-                                       $destTitle->getPrefixedText() );
-                       }
-                       $mwRedir = MagicWord::get( 'redirect' );
-                       $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n";
-                       $redirectArticle = new Article( $targetTitle );
-                       $redirectRevision = new Revision( array(
-                               'page'    => $this->mTargetID,
-                               'comment' => $comment,
-                               'text'    => $redirectText ) );
-                       $redirectRevision->insertOn( $dbw );
-                       $redirectArticle->updateRevisionOn( $dbw, $redirectRevision );
-
-                       # Now, we record the link from the redirect to the new title.
-                       # It should have no other outgoing links...
-                       $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
-                       $dbw->insert( 'pagelinks',
-                               array(
-                                       'pl_from'      => $this->mDestID,
-                                       'pl_namespace' => $destTitle->getNamespace(),
-                                       'pl_title'     => $destTitle->getDBkey() ),
-                               __METHOD__ );
-               } else {
-                       $targetTitle->invalidateCache(); // update histories
-               }
-               $destTitle->invalidateCache(); // update histories
-               # Check if this did anything
-               if( !$count ) {
-                       $wgOut->addWikiMsg('mergehistory-fail');
-                       return false;
-               }
-               # Update our logs
-               $log = new LogPage( 'merge' );
-               $log->addEntry( 'merge', $targetTitle, $this->mComment,
-                       array($destTitle->getPrefixedText(),$TimestampLimit) );
-
-               $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
-                       $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
-
-               wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
-
-               return true;
-       }
-}
-
-class MergeHistoryPager extends ReverseChronologicalPager {
-       public $mForm, $mConds;
-
-       function __construct( $form, $conds = array(), $source, $dest ) {
-               $this->mForm = $form;
-               $this->mConds = $conds;
-               $this->title = $source;
-               $this->articleID = $source->getArticleID();
-
-               $dbr = wfGetDB( DB_SLAVE );
-               $maxtimestamp = $dbr->selectField( 'revision', 'MIN(rev_timestamp)',
-                       array('rev_page' => $dest->getArticleID() ),
-                       __METHOD__ );
-               $this->maxTimestamp = $maxtimestamp;
-
-               parent::__construct();
-       }
-
-       function getStartBody() {
-               wfProfileIn( __METHOD__ );
-               # Do a link batch query
-               $this->mResult->seek( 0 );
-               $batch = new LinkBatch();
-               # Give some pointers to make (last) links
-               $this->mForm->prevId = array();
-               while( $row = $this->mResult->fetchObject() ) {
-                       $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );
-                       $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );
-
-                       $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;
-                       if( $rev_id > $row->rev_id )
-                               $this->mForm->prevId[$rev_id] = $row->rev_id;
-                       else if( $rev_id < $row->rev_id )
-                               $this->mForm->prevId[$row->rev_id] = $rev_id;
-
-                       $rev_id = $row->rev_id;
-               }
-
-               $batch->execute();
-               $this->mResult->seek( 0 );
-
-               wfProfileOut( __METHOD__ );
-               return '';
-       }
-
-       function formatRow( $row ) {
-               $block = new Block;
-               return $this->mForm->formatRevisionRow( $row );
-       }
-
-       function getQueryInfo() {
-               $conds = $this->mConds;
-               $conds['rev_page'] = $this->articleID;
-               $conds[] = "rev_timestamp < {$this->maxTimestamp}";
-
-               return array(
-                       'tables' => array('revision'),
-                       'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment',
-                                'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),
-                       'conds' => $conds
-               );
-       }
-
-       function getIndexField() {
-               return 'rev_timestamp';
-       }
-}
diff --git a/includes/SpecialMissingFiles.php b/includes/SpecialMissingFiles.php
deleted file mode 100644 (file)
index d2da51c..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-/**
- * A querypage to list the missing files - implements Special:Missingfiles
- *
- * @addtogroup SpecialPage
- *
- * @author MatÄ›j Grabovský <65s.mg@atlas.cz>
- * @copyright Copyright Â© 2008, MatÄ›j Grabovský
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-class MissingFilesPage extends QueryPage {
-       function getName() {
-               return 'Missingfiles';
-       }
-           
-       function isExpensive() {
-               return true;
-       }
-           
-       function isSyndicated() {
-               return false;
-       }
-       
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $imagelinks, $page ) = $dbr->tableNamesN( 'imagelinks', 'page' );
-               $name = $dbr->addQuotes( $this->getName() );
-               
-               return "SELECT $name as type,
-                        " . NS_IMAGE . " as namespace,
-                        il_to as title,
-                        COUNT(*) as value
-                        FROM $imagelinks
-                        LEFT JOIN $page ON il_to = page_title AND page_namespace = ". NS_IMAGE ."
-                        WHERE page_title IS NULL
-                        GROUP BY 1,2,3
-               ";
-       }
-       
-       function sortDescending() {
-               return true;
-       }
-       
-       /**
-        * Fetch user page links and cache their existence
-        */
-       function preprocessResults( $db, $res ) {
-               $batch = new LinkBatch;
-               
-               while ( $row = $db->fetchObject( $res ) )
-                       $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
-               
-               $batch->execute();
-               
-               // Back to start for display
-               if ( $db->numRows( $res ) > 0 )
-               
-               // If there are no rows we get an error seeking.
-               $db->dataSeek( $res, 0 );
-       }
-       
-       public function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-               
-               $nt = Title::makeTitle( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getText() );
-               
-               $plink = $this->isCached() 
-                       ? '<s>' . $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) . '</s>'
-                       : $skin->makeBrokenImageLinkObj( $nt, htmlspecialchars( $text ) );
-               
-               $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
-               $nlinks = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Whatlinkshere' ), $label, 'target=' . $nt->getPrefixedUrl() );
-               return wfSpecialList( $plink, $nlinks );
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialMissingFiles() {
-       list( $limit, $offset ) = wfCheckLimits();
-       
-       $wpp = new MissingFilesPage();
-       
-       $wpp->doQuery( $offset, $limit );
-}
\ No newline at end of file
diff --git a/includes/SpecialMostcategories.php b/includes/SpecialMostcategories.php
deleted file mode 100644 (file)
index 5df9c86..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * implements Special:Mostcategories
- * @ingroup SpecialPage
- */
-class MostcategoriesPage extends QueryPage {
-
-       function getName() { return 'Mostcategories'; }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $categorylinks, $page) = $dbr->tableNamesN( 'categorylinks', 'page' );
-               return
-                       "
-                       SELECT
-                               'Mostcategories' as type,
-                               page_namespace as namespace,
-                               page_title as title,
-                               COUNT(*) as value
-                       FROM $categorylinks
-                       LEFT JOIN $page ON cl_from = page_id
-                       WHERE page_namespace = " . NS_MAIN . "
-                       GROUP BY 1,2,3
-                       HAVING COUNT(*) > 1
-                       ";
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang;
-               $title = Title::makeTitleSafe( $result->namespace, $result->title );
-               if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); }
-               $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
-               $link = $skin->makeKnownLinkObj( $title, $title->getText() );
-               return wfSpecialList( $link, $count );
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialMostcategories() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new MostcategoriesPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMostimages.php b/includes/SpecialMostimages.php
deleted file mode 100644 (file)
index 2fed0bd..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * implements Special:Mostimages
- * @ingroup SpecialPage
- */
-class MostimagesPage extends ImageQueryPage {
-
-       function getName() { return 'Mostimages'; }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $imagelinks = $dbr->tableName( 'imagelinks' );
-               return
-                       "
-                       SELECT
-                               'Mostimages' as type,
-                               " . NS_IMAGE . " as namespace,
-                               il_to as title,
-                               COUNT(*) as value
-                       FROM $imagelinks
-                       GROUP BY 1,2,3
-                       HAVING COUNT(*) > 1
-                       ";
-       }
-
-       function getCellHtml( $row ) {
-               global $wgLang;
-               return wfMsgExt( 'nlinks',  array( 'parsemag', 'escape' ),
-                       $wgLang->formatNum( $row->value ) ) . '<br />';
-       }
-
-}
-
-/**
- * Constructor
- */
-function wfSpecialMostimages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new MostimagesPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php
deleted file mode 100644 (file)
index a56ac26..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page to show pages ordered by the number of pages linking to them.
- * Implements Special:Mostlinked
- *
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @author Rob Church <robchur@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @copyright Â© 2006 Rob Church
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-class MostlinkedPage extends QueryPage {
-
-       function getName() { return 'Mostlinked'; }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       /**
-        * Note: Getting page_namespace only works if $this->isCached() is false
-        */
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $pagelinks, $page ) = $dbr->tableNamesN( 'pagelinks', 'page' );
-               return
-                       "SELECT 'Mostlinked' AS type,
-                               pl_namespace AS namespace,
-                               pl_title AS title,
-                               COUNT(*) AS value,
-                               page_namespace
-                       FROM $pagelinks
-                       LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title
-                       GROUP BY 1,2,3,5
-                       HAVING COUNT(*) > 1";
-       }
-
-       /**
-        * Pre-fill the link cache
-        */
-       function preprocessResults( $db, $res ) {
-               if( $db->numRows( $res ) > 0 ) {
-                       $linkBatch = new LinkBatch();
-                       while( $row = $db->fetchObject( $res ) )
-                               $linkBatch->add( $row->namespace, $row->title );
-                       $db->dataSeek( $res, 0 );
-                       $linkBatch->execute();
-               }
-       }
-
-       /**
-        * Make a link to "what links here" for the specified title
-        *
-        * @param $title Title being queried
-        * @param $skin Skin to use
-        * @return string
-        */
-       function makeWlhLink( &$title, $caption, &$skin ) {
-               $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
-               return $skin->makeKnownLinkObj( $wlh, $caption );
-       }
-
-       /**
-        * Make links to the page corresponding to the item, and the "what links here" page for it
-        *
-        * @param $skin Skin to be used
-        * @param $result Result row
-        * @return string
-        */
-       function formatResult( $skin, $result ) {
-               global $wgLang;
-               $title = Title::makeTitleSafe( $result->namespace, $result->title );
-               $link = $skin->makeLinkObj( $title );
-               $wlh = $this->makeWlhLink( $title,
-                       wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
-                               $wgLang->formatNum( $result->value ) ), $skin );
-               return wfSpecialList( $link, $wlh );
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialMostlinked() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new MostlinkedPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php
deleted file mode 100644 (file)
index 1b66d48..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A querypage to show categories ordered in descending order by the pages  in them
- *
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-class MostlinkedCategoriesPage extends QueryPage {
-
-       function getName() { return 'Mostlinkedcategories'; }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $categorylinks = $dbr->tableName( 'categorylinks' );
-               $name = $dbr->addQuotes( $this->getName() );
-               return
-                       "
-                       SELECT
-                               $name as type,
-                               " . NS_CATEGORY . " as namespace,
-                               cl_to as title,
-                               COUNT(*) as value
-                       FROM $categorylinks
-                       GROUP BY 1,2,3
-                       ";
-       }
-
-       function sortDescending() { return true; }
-
-       /**
-        * Fetch user page links and cache their existence
-        */
-       function preprocessResults( $db, $res ) {
-               $batch = new LinkBatch;
-               while ( $row = $db->fetchObject( $res ) )
-                       $batch->add( $row->namespace, $row->title );
-               $batch->execute();
-
-               // Back to start for display
-               if ( $db->numRows( $res ) > 0 )
-                       // If there are no rows we get an error seeking.
-                       $db->dataSeek( $res, 0 );
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-
-               $nt = Title::makeTitle( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getText() );
-
-               $plink = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) );
-
-               $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
-                       $wgLang->formatNum( $result->value ) );
-               return wfSpecialList($plink, $nlinks);
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialMostlinkedCategories() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new MostlinkedCategoriesPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMostlinkedtemplates.php b/includes/SpecialMostlinkedtemplates.php
deleted file mode 100644 (file)
index b8d47e6..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-/**
- * Special page lists templates with a large number of
- * transclusion links, i.e. "most used" templates
- *
- * @ingroup SpecialPage
- * @author Rob Church <robchur@gmail.com>
- */
-class SpecialMostlinkedtemplates extends QueryPage {
-
-       /**
-        * Name of the report
-        *
-        * @return string
-        */
-       public function getName() {
-               return 'Mostlinkedtemplates';
-       }
-
-       /**
-        * Is this report expensive, i.e should it be cached?
-        *
-        * @return bool
-        */
-       public function isExpensive() {
-               return true;
-       }
-
-       /**
-        * Is there a feed available?
-        *
-        * @return bool
-        */
-       public function isSyndicated() {
-               return false;
-       }
-
-       /**
-        * Sort the results in descending order?
-        *
-        * @return bool
-        */
-       public function sortDescending() {
-               return true;
-       }
-
-       /**
-        * Generate SQL for the report
-        *
-        * @return string
-        */
-       public function getSql() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $templatelinks = $dbr->tableName( 'templatelinks' );
-               $name = $dbr->addQuotes( $this->getName() );
-               return "SELECT {$name} AS type,
-                       " . NS_TEMPLATE . " AS namespace,
-                       tl_title AS title,
-                       COUNT(*) AS value
-                       FROM {$templatelinks}
-                       WHERE tl_namespace = " . NS_TEMPLATE . "
-                       GROUP BY 1, 2, 3";
-       }
-
-       /**
-        * Pre-cache page existence to speed up link generation
-        *
-        * @param Database $dbr Database connection
-        * @param int $res Result pointer
-        */
-       public function preprocessResults( $db, $res ) {
-               $batch = new LinkBatch();
-               while( $row = $db->fetchObject( $res ) ) {
-                       $batch->add( $row->namespace, $row->title );
-               }
-               $batch->execute();
-               if( $db->numRows( $res ) > 0 )
-                       $db->dataSeek( $res, 0 );
-       }
-
-       /**
-        * Format a result row
-        *
-        * @param Skin $skin Skin to use for UI elements
-        * @param object $result Result row
-        * @return string
-        */
-       public function formatResult( $skin, $result ) {
-               $title = Title::makeTitleSafe( $result->namespace, $result->title );
-               if( $title instanceof Title ) {
-                       return wfSpecialList(
-                               $skin->makeLinkObj( $title ),
-                               $this->makeWlhLink( $title, $skin, $result )
-                       );
-               } else {
-                       $tsafe = htmlspecialchars( $result->title );
-                       return "Invalid title in result set; {$tsafe}";
-               }
-       }
-
-       /**
-        * Make a "what links here" link for a given title
-        *
-        * @param Title $title Title to make the link for
-        * @param Skin $skin Skin to use
-        * @param object $result Result row
-        * @return string
-        */
-       private function makeWlhLink( $title, $skin, $result ) {
-               global $wgLang;
-               $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
-               $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
-                       $wgLang->formatNum( $result->value ) );
-               return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
-       }
-}
-
-/**
- * Execution function
- *
- * @param mixed $par Parameters passed to the page
- */
-function wfSpecialMostlinkedtemplates( $par = false ) {
-       list( $limit, $offset ) = wfCheckLimits();
-       $mlt = new SpecialMostlinkedtemplates();
-       $mlt->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMostrevisions.php b/includes/SpecialMostrevisions.php
deleted file mode 100644 (file)
index 001a08b..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * A special page to show pages in the
- *
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * @ingroup SpecialPage
- */
-class MostrevisionsPage extends QueryPage {
-
-       function getName() { return 'Mostrevisions'; }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
-               return
-                       "
-                       SELECT
-                               'Mostrevisions' as type,
-                               page_namespace as namespace,
-                               page_title as title,
-                               COUNT(*) as value
-                       FROM $revision
-                       JOIN $page ON page_id = rev_page
-                       WHERE page_namespace = " . NS_MAIN . "
-                       GROUP BY 1,2,3
-                       HAVING COUNT(*) > 1
-                       ";
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-
-               $nt = Title::makeTitle( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getPrefixedText() );
-
-               $plink = $skin->makeKnownLinkObj( $nt, $text );
-
-               $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
-                       $wgLang->formatNum( $result->value ) );
-               $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
-
-               return wfSpecialList($plink, $nlink);
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialMostrevisions() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new MostrevisionsPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php
deleted file mode 100644 (file)
index d08fb66..0000000
+++ /dev/null
@@ -1,428 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor
- */
-function wfSpecialMovepage( $par = null ) {
-       global $wgUser, $wgOut, $wgRequest, $action;
-
-       # Check for database lock
-       if ( wfReadOnly() ) {
-               $wgOut->readOnlyPage();
-               return;
-       }
-
-       $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
-       $oldTitle = $wgRequest->getText( 'wpOldTitle', $target );
-       $newTitle = $wgRequest->getText( 'wpNewTitle' );
-
-       # Variables beginning with 'o' for old article 'n' for new article
-       $ot = Title::newFromText( $oldTitle );
-       $nt = Title::newFromText( $newTitle );
-
-       if( is_null( $ot ) ) {
-               $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
-               return;
-       }
-       if( !$ot->exists() ) {
-               $wgOut->showErrorPage( 'nopagetitle', 'nopagetext' );
-               return;
-       }
-
-       # Check rights
-       $permErrors = $ot->getUserPermissionsErrors( 'move', $wgUser );
-       if( !empty( $permErrors ) ) {
-               $wgOut->showPermissionsErrorPage( $permErrors );
-               return;
-       }
-
-       $f = new MovePageForm( $ot, $nt );
-
-       if ( 'submit' == $action && $wgRequest->wasPosted()
-               && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
-               $f->doSubmit();
-       } else {
-               $f->showForm( '' );
-       }
-}
-
-/**
- * HTML form for Special:Movepage
- * @ingroup SpecialPage
- */
-class MovePageForm {
-       var $oldTitle, $newTitle, $reason; # Text input
-       var $moveTalk, $deleteAndMove, $moveSubpages;
-
-       private $watch = false;
-
-       function MovePageForm( $oldTitle, $newTitle ) {
-               global $wgRequest;
-               $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
-               $this->oldTitle = $oldTitle;
-               $this->newTitle = $newTitle;
-               $this->reason = $wgRequest->getText( 'wpReason' );
-               if ( $wgRequest->wasPosted() ) {
-                       $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false );
-               } else {
-                       $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
-               }
-               $this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false );
-               $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
-               $this->watch = $wgRequest->getCheck( 'wpWatch' );
-       }
-
-       function showForm( $err, $hookErr = '' ) {
-               global $wgOut, $wgUser;
-
-               $ot = $this->oldTitle;
-               $sk = $wgUser->getSkin();
-
-               $oldTitleLink = $sk->makeLinkObj( $ot );
-               $oldTitle = $ot->getPrefixedText();
-
-               $wgOut->setPagetitle( wfMsg( 'move-page', $oldTitle ) );
-               $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
-
-               if( $this->newTitle == '' ) {
-                       # Show the current title as a default
-                       # when the form is first opened.
-                       $newTitle = $oldTitle;
-               } else {
-                       if( $err == '' ) {
-                               $nt = Title::newFromURL( $this->newTitle );
-                               if( $nt ) {
-                                       # If a title was supplied, probably from the move log revert
-                                       # link, check for validity. We can then show some diagnostic
-                                       # information and save a click.
-                                       $newerr = $ot->isValidMoveOperation( $nt );
-                                       if( is_string( $newerr ) ) {
-                                               $err = $newerr;
-                                       }
-                               }
-                       }
-                       $newTitle = $this->newTitle;
-               }
-
-               if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
-                       $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle );
-                       $movepagebtn = wfMsg( 'delete_and_move' );
-                       $submitVar = 'wpDeleteAndMove';
-                       $confirm = "
-                               <tr>
-                                       <td></td>
-                                       <td class='mw-input'>" .
-                                               Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) .
-                                       "</td>
-                               </tr>";
-                       $err = '';
-               } else {
-                       $wgOut->addWikiMsg( 'movepagetext' );
-                       $movepagebtn = wfMsg( 'movepagebtn' );
-                       $submitVar = 'wpMove';
-                       $confirm = false;
-               }
-
-               $oldTalk = $ot->getTalkPage();
-               $considerTalk = ( !$ot->isTalkPage() && $oldTalk->exists() );
-
-               if ( $considerTalk ) {
-                       $wgOut->addWikiMsg( 'movepagetalktext' );
-               }
-
-               $titleObj = SpecialPage::getTitleFor( 'Movepage' );
-               $token = htmlspecialchars( $wgUser->editToken() );
-
-               if ( $err != '' ) {
-                       $wgOut->setSubtitle( wfMsg( 'formerror' ) );
-                       $errMsg = "";
-                       if( $err == 'hookaborted' ) {
-                               $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
-                       } else if (is_array($err)) {
-                               $errMsg = '<p><strong class="error">' . call_user_func_array( 'wfMsgWikiHtml', $err ) . "</strong></p>\n";
-                       } else {
-                               $errMsg = '<p><strong class="error">' . wfMsgWikiHtml( $err ) . "</strong></p>\n";
-                       }
-                       $wgOut->addHTML( $errMsg );
-               }
-
-               $wgOut->addHTML(
-                        Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
-                        Xml::openElement( 'fieldset' ) .
-                        Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) .
-                        Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) .
-                        "<tr>
-                               <td class='mw-label'>" .
-                                       wfMsgHtml( 'movearticle' ) .
-                               "</td>
-                               <td class='mw-input'>
-                                       <strong>{$oldTitleLink}</strong>
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                                       Xml::input( 'wpNewTitle', 40, $newTitle, array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
-                                       Xml::hidden( 'wpOldTitle', $oldTitle ) .
-                               "</td>
-                       </tr>
-                       <tr>
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'movereason' ), 'wpReason' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                                       Xml::tags( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2 ), htmlspecialchars( $this->reason ) ) .
-                               "</td>
-                       </tr>"
-               );
-
-               if( $considerTalk ) {
-                       $wgOut->addHTML( "
-                               <tr>
-                                       <td></td>
-                                       <td class='mw-input'>" .
-                                               Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
-                                       "</td>
-                               </tr>"
-                       );
-               }
-
-               if( ($ot->hasSubpages() || $ot->getTalkPage()->hasSubpages())
-               && $ot->userCan( 'move-subpages' ) ) {
-                       $wgOut->addHTML( "
-                               <tr>
-                                       <td></td>
-                                       <td class=\"mw-input\">" .
-                               Xml::checkLabel( wfMsgHtml(
-                                               $ot->hasSubpages()
-                                               ? 'move-subpages'
-                                               : 'move-talk-subpages'
-                                       ),
-                                       'wpMovesubpages', 'wpMovesubpages',
-                                       # Don't check the box if we only have talk subpages to
-                                       # move and we aren't moving the talk page.
-                                       $this->moveSubpages && ($ot->hasSubpages() || $this->moveTalk)
-                               ) .
-                                       "</td>
-                               </tr>"
-                       );
-               }
-
-               $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) || $ot->userIsWatching();
-               $wgOut->addHTML( "
-                       <tr>
-                               <td></td>
-                               <td class='mw-input'>" .
-                                       Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
-                               "</td>
-                       </tr>
-                               {$confirm}
-                       <tr>
-                               <td>&nbsp;</td>
-                               <td class='mw-submit'>" .
-                                       Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
-                               "</td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::hidden( 'wpEditToken', $token ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) .
-                       "\n"
-               );
-
-               $this->showLogFragment( $ot, $wgOut );
-
-       }
-
-       function doSubmit() {
-               global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang;
-
-               if ( $wgUser->pingLimiter( 'move' ) ) {
-                       $wgOut->rateLimited();
-                       return;
-               }
-
-               $ot = $this->oldTitle;
-               $nt = $this->newTitle;
-
-               # Delete to make way if requested
-               if ( $wgUser->isAllowed( 'delete' ) && $this->deleteAndMove ) {
-                       $article = new Article( $nt );
-
-                       # Disallow deletions of big articles
-                       $bigHistory = $article->isBigDeletion();
-                       if( $bigHistory && !$nt->userCan( 'bigdelete' ) ) {
-                               global $wgLang, $wgDeleteRevisionsLimit;
-                               $this->showForm( array('delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
-                               return;
-                       }
-
-                       // This may output an error message and exit
-                       $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
-               }
-
-               # don't allow moving to pages with # in
-               if ( !$nt || $nt->getFragment() != '' ) {
-                       $this->showForm( 'badtitletext' );
-                       return;
-               }
-
-               $error = $ot->moveTo( $nt, true, $this->reason );
-               if ( $error !== true ) {
-                       # FIXME: showForm() should handle multiple errors
-                       call_user_func_array(array($this, 'showForm'), $error[0]);
-                       return;
-               }
-
-               wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ;
-
-               $wgOut->setPagetitle( wfMsg( 'pagemovedsub' ) );
-
-               $oldUrl = $ot->getFullUrl( 'redirect=no' );
-               $newUrl = $nt->getFullUrl();
-               $oldText = $ot->getPrefixedText();
-               $newText = $nt->getPrefixedText();
-               $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
-               $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
-
-               $wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
-
-               # Now we move extra pages we've been asked to move: subpages and talk
-               # pages.  First, if the old page or the new page is a talk page, we
-               # can't move any talk pages: cancel that.
-               if( $ot->isTalkPage() || $nt->isTalkPage() ) {
-                       $this->moveTalk = false;
-               }
-
-               if( !$ot->userCan( 'move-subpages' ) ) {
-                       $this->moveSubpages = false;
-               }
-
-               # Next make a list of id's.  This might be marginally less efficient
-               # than a more direct method, but this is not a highly performance-cri-
-               # tical code path and readable code is more important here.
-               #
-               # Note: this query works nicely on MySQL 5, but the optimizer in MySQL
-               # 4 might get confused.  If so, consider rewriting as a UNION.
-               #
-               # If the target namespace doesn't allow subpages, moving with subpages
-               # would mean that you couldn't move them back in one operation, which
-               # is bad.  FIXME: A specific error message should be given in this
-               # case.
-               $dbr = wfGetDB( DB_MASTER );
-               if( $this->moveSubpages && (
-                       MWNamespace::hasSubpages( $nt->getNamespace() ) || (
-                               $this->moveTalk &&
-                               MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
-                       )
-               ) ) {
-                       $conds = array(
-                               'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' )
-                                       .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
-                       );
-                       $conds['page_namespace'] = array();
-                       if( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
-                               $conds['page_namespace'] []= $ot->getNamespace();
-                       }
-                       if( $this->moveTalk && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) {
-                               $conds['page_namespace'] []= $ot->getTalkPage()->getNamespace();
-                       }
-               } elseif( $this->moveTalk ) {
-                       $conds = array(
-                               'page_namespace' => $ot->getTalkPage()->getNamespace(),
-                               'page_title' => $ot->getDBKey()
-                       );
-               } else {
-                       # Skip the query
-                       $conds = null;
-               }
-
-               $extrapages = array();
-               if( !is_null( $conds ) ) {
-                       $extrapages = $dbr->select( 'page',
-                               array( 'page_id', 'page_namespace', 'page_title' ),
-                               $conds,
-                               __METHOD__
-                       );
-               }
-
-               $extraOutput = array();
-               $skin = $wgUser->getSkin();
-               $count = 1;
-               foreach( $extrapages as $row ) {
-                       if( $row->page_id == $ot->getArticleId() ) {
-                               # Already did this one.
-                               continue;
-                       }
-
-                       $oldPage = Title::newFromRow( $row );
-                       $newPageName = preg_replace(
-                               '#^'.preg_quote( $ot->getDBKey(), '#' ).'#',
-                               $nt->getDBKey(),
-                               $oldPage->getDBKey()
-                       );
-                       if( $oldPage->isTalkPage() ) {
-                               $newNs = $nt->getTalkPage()->getNamespace();
-                       } else {
-                               $newNs = $nt->getSubjectPage()->getNamespace();
-                       }
-                       # Bug 14385: we need makeTitleSafe because the new page names may
-                       # be longer than 255 characters.
-                       $newPage = Title::makeTitleSafe( $newNs, $newPageName );
-                       if( !$newPage ) {
-                               $oldLink = $skin->makeKnownLinkObj( $oldPage );
-                               $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
-                                       htmlspecialchars(Title::makeName( $newNs, $newPageName )));
-                               continue;
-                       }
-
-                       # This was copy-pasted from Renameuser, bleh.
-                       if ( $newPage->exists() && !$oldPage->isValidMoveTarget( $newPage ) ) {
-                               $link = $skin->makeKnownLinkObj( $newPage );
-                               $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
-                       } else {
-                               $success = $oldPage->moveTo( $newPage, true, $this->reason );
-                               if( $success === true ) {
-                                       $oldLink = $skin->makeKnownLinkObj( $oldPage, '', 'redirect=no' );
-                                       $newLink = $skin->makeKnownLinkObj( $newPage );
-                                       $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
-                               } else {
-                                       $oldLink = $skin->makeKnownLinkObj( $oldPage );
-                                       $newLink = $skin->makeLinkObj( $newPage );
-                                       $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink );
-                               }
-                       }
-
-                       ++$count;
-                       if( $count >= $wgMaximumMovedPages ) {
-                               $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) );
-                               break;
-                       }
-               }
-
-               if( $extraOutput !== array() ) {
-                       $wgOut->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
-               }
-
-               # Deal with watches (we don't watch subpages)
-               if( $this->watch ) {
-                       $wgUser->addWatch( $ot );
-                       $wgUser->addWatch( $nt );
-               } else {
-                       $wgUser->removeWatch( $ot );
-                       $wgUser->removeWatch( $nt );
-               }
-       }
-
-       function showLogFragment( $title, &$out ) {
-               $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'move' ) ) );
-               LogEventsList::showLogExtract( $out, 'move', $title->getPrefixedText() );
-       }
-
-}
diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php
deleted file mode 100644 (file)
index 5fd37e8..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- * FIXME: this code is crap, should use Pager and Database::select().
- */
-
-/**
- *
- */
-function wfSpecialNewimages( $par, $specialPage ) {
-       global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions, $wgMiserMode;
-
-       $wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
-       $dbr = wfGetDB( DB_SLAVE );
-       $sk = $wgUser->getSkin();
-       $shownav = !$specialPage->including();
-       $hidebots = $wgRequest->getBool('hidebots',1);
-
-       $hidebotsql = '';
-       if ($hidebots) {
-
-               /** Make a list of group names which have the 'bot' flag
-                   set.
-               */
-               $botconds=array();
-               foreach ($wgGroupPermissions as $groupname=>$perms) {
-                       if(array_key_exists('bot',$perms) && $perms['bot']) {
-                               $botconds[]="ug_group='$groupname'";
-                       }
-               }
-
-               /* If not bot groups, do not set $hidebotsql */
-               if ($botconds) {
-                       $isbotmember=$dbr->makeList($botconds, LIST_OR);
-
-                       /** This join, in conjunction with WHERE ug_group
-                           IS NULL, returns only those rows from IMAGE
-                       where the uploading user is not a member of
-                       a group which has the 'bot' permission set.
-                       */
-                       $ug = $dbr->tableName('user_groups');
-                       $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)";
-               }
-       }
-
-       $image = $dbr->tableName('image');
-
-       $sql="SELECT img_timestamp from $image";
-       if ($hidebotsql) {
-               $sql .= "$hidebotsql WHERE ug_group IS NULL";
-       }
-       $sql.=' ORDER BY img_timestamp DESC LIMIT 1';
-       $res = $dbr->query($sql, 'wfSpecialNewImages');
-       $row = $dbr->fetchRow($res);
-       if($row!==false) {
-               $ts=$row[0];
-       } else {
-               $ts=false;
-       }
-       $dbr->freeResult($res);
-       $sql='';
-
-       /** If we were clever, we'd use this to cache. */
-       $latestTimestamp = wfTimestamp( TS_MW, $ts);
-
-       /** Hardcode this for now. */
-       $limit = 48;
-
-       if ( $parval = intval( $par ) ) {
-               if ( $parval <= $limit && $parval > 0 ) {
-                       $limit = $parval;
-               }
-       }
-
-       $where = array();
-       $searchpar = '';
-       if ( $wpIlMatch != '' && !$wgMiserMode) {
-               $nt = Title::newFromUrl( $wpIlMatch );
-               if($nt ) {
-                       $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
-                       $m = str_replace( '%', "\\%", $m );
-                       $m = str_replace( '_', "\\_", $m );
-                       $where[] = "LOWER(img_name) LIKE '%{$m}%'";
-                       $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
-               }
-       }
-
-       $invertSort = false;
-       if( $until = $wgRequest->getVal( 'until' ) ) {
-               $where[] = "img_timestamp < '" . $dbr->timestamp( $until ) . "'";
-       }
-       if( $from = $wgRequest->getVal( 'from' ) ) {
-               $where[] = "img_timestamp >= '" . $dbr->timestamp( $from ) . "'";
-               $invertSort = true;
-       }
-       $sql='SELECT img_size, img_name, img_user, img_user_text,'.
-            "img_description,img_timestamp FROM $image";
-
-       if($hidebotsql) {
-               $sql .= $hidebotsql;
-               $where[]='ug_group IS NULL';
-       }
-       if(count($where)) {
-               $sql.=' WHERE '.$dbr->makeList($where, LIST_AND);
-       }
-       $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' );
-       $sql.=' LIMIT '.($limit+1);
-       $res = $dbr->query($sql, 'wfSpecialNewImages');
-
-       /**
-        * We have to flip things around to get the last N after a certain date
-        */
-       $images = array();
-       while ( $s = $dbr->fetchObject( $res ) ) {
-               if( $invertSort ) {
-                       array_unshift( $images, $s );
-               } else {
-                       array_push( $images, $s );
-               }
-       }
-       $dbr->freeResult( $res );
-
-       $gallery = new ImageGallery();
-       $firstTimestamp = null;
-       $lastTimestamp = null;
-       $shownImages = 0;
-       foreach( $images as $s ) {
-               if( ++$shownImages > $limit ) {
-                       # One extra just to test for whether to show a page link;
-                       # don't actually show it.
-                       break;
-               }
-
-               $name = $s->img_name;
-               $ut = $s->img_user_text;
-
-               $nt = Title::newFromText( $name, NS_IMAGE );
-               $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
-
-               $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
-
-               $timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
-               if( empty( $firstTimestamp ) ) {
-                       $firstTimestamp = $timestamp;
-               }
-               $lastTimestamp = $timestamp;
-       }
-
-       $bydate = wfMsg( 'bydate' );
-       $lt = $wgLang->formatNum( min( $shownImages, $limit ) );
-       if ($shownav) {
-               $text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate );
-               $wgOut->addHTML( $text . "\n" );
-       }
-
-       $sub = wfMsg( 'ilsubmit' );
-       $titleObj = SpecialPage::getTitleFor( 'Newimages' );
-       $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' );
-       if ($shownav && !$wgMiserMode) {
-               $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" .
-                 "{$action}\">" .
-                       Xml::input( 'wpIlMatch', 20, $wpIlMatch ) . ' ' .
-                 Xml::submitButton( $sub, array( 'name' => 'wpIlSubmit' ) ) .
-                 "</form>" );
-       }
-
-       /**
-        * Paging controls...
-        */
-
-       # If we change bot visibility, this needs to be carried along.
-       if(!$hidebots) {
-               $botpar='&hidebots=0';
-       } else {
-               $botpar='';
-       }
-       $now = wfTimestampNow();
-       $d = $wgLang->date( $now, true );
-       $t = $wgLang->time( $now, true );
-       $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'sp-newimages-showfrom', $d, $t ), 
-               'from='.$now.$botpar.$searchpar );
-
-       $botLink = $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'showhidebots', 
-               ($hidebots ? wfMsgHtml('show') : wfMsgHtml('hide'))),'hidebots='.($hidebots ? '0' : '1').$searchpar);
-
-       $prevLink = wfMsgHtml( 'prevn', $wgLang->formatNum( $limit ) );
-       if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
-               $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
-       }
-
-       $nextLink = wfMsgHtml( 'nextn', $wgLang->formatNum( $limit ) );
-       if( $shownImages > $limit && $lastTimestamp ) {
-               $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
-       }
-
-       $prevnext = '<p>' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>';
-
-       if ($shownav)
-               $wgOut->addHTML( $prevnext );
-
-       if( count( $images ) ) {
-               $wgOut->addHTML( $gallery->toHTML() );
-               if ($shownav)
-                       $wgOut->addHTML( $prevnext );
-       } else {
-               $wgOut->addWikiMsg( 'noimages' );
-       }
-}
diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php
deleted file mode 100644 (file)
index 06252a8..0000000
+++ /dev/null
@@ -1,445 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-
-/**
- * Start point
- */
-function wfSpecialNewPages( $par, $sp ) {
-       $page = new NewPagesForm();
-       $page->execute( $par, $sp->including() );
-}
-
-/**
- * implements Special:Newpages
- * @ingroup SpecialPage
- */
-class NewPagesForm {
-
-       // Stored objects
-       protected $opts, $title, $skin;
-
-       // Some internal settings
-       protected $showNavigation = false;
-
-       protected function setup( $par ) {
-               global $wgRequest, $wgUser, $wgEnableNewpagesUserFilter;
-
-               // Options
-               $opts = new FormOptions();
-               $this->opts = $opts; // bind
-               $opts->add( 'hideliu', false );
-               $opts->add( 'hidepatrolled', false );
-               $opts->add( 'hidebots', false );
-               $opts->add( 'limit', 50 );
-               $opts->add( 'offset', '' );
-               $opts->add( 'namespace', '0' );
-               $opts->add( 'username', '' );
-               $opts->add( 'feed', '' );
-
-               // Set values
-               $opts->fetchValuesFromRequest( $wgRequest );
-               if ( $par ) $this->parseParams( $par );
-
-               // Validate
-               $opts->validateIntBounds( 'limit', 0, 5000 );
-               if( !$wgEnableNewpagesUserFilter ) {
-                       $opts->setValue( 'username', '' );
-               }
-
-               // Store some objects
-               $this->skin = $wgUser->getSkin();
-               $this->title = SpecialPage::getTitleFor( 'NewPages' );
-       }
-
-       protected function parseParams( $par ) {
-               global $wgLang;
-               $bits = preg_split( '/\s*,\s*/', trim( $par ) );
-               foreach ( $bits as $bit ) {
-                       if ( 'shownav' == $bit )
-                               $this->showNavigation = true;
-                       if ( 'hideliu' === $bit )
-                               $this->opts->setValue( 'hideliu', true );
-                       if ( 'hidepatrolled' == $bit )
-                               $this->opts->setValue( 'hidepatrolled', true );
-                       if ( 'hidebots' == $bit )
-                               $this->opts->setValue( 'hidebots', true );
-                       if ( is_numeric( $bit ) )
-                               $this->opts->setValue( 'limit', intval( $bit ) );
-
-                       $m = array();
-                       if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
-                               $this->opts->setValue( 'limit', intval($m[1]) );
-                       // PG offsets not just digits!
-                       if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) )
-                               $this->opts->setValue( 'offset',  intval($m[1]) );
-                       if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
-                               $ns = $wgLang->getNsIndex( $m[1] );
-                               if( $ns !== false ) {
-                                       $this->opts->setValue( 'namespace',  $ns );
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Show a form for filtering namespace and username
-        *
-        * @param string $par
-        * @param bool $including true if the page is being included with {{Special:Newpages}}
-        * @return string
-        */
-       public function execute( $par, $including ) {
-               global $wgLang, $wgGroupPermissions, $wgUser, $wgOut;
-
-               $this->showNavigation = !$including; // Maybe changed in setup
-               $this->setup( $par );
-
-               if( !$including ) {
-                       // Settings
-                       $this->form();
-
-                       $this->setSyndicated();
-                       $feedType = $this->opts->getValue( 'feed' );
-                       if( $feedType ) {
-                               return $this->feed( $feedType );
-                       }
-               }
-
-               $pager = new NewPagesPager( $this, $this->opts );
-               $pager->mLimit = $this->opts->getValue( 'limit' );
-               $pager->mOffset = $this->opts->getValue( 'offset' );
-
-               if( $pager->getNumRows() ) {
-                       $navigation = '';
-                       if ( $this->showNavigation ) $navigation = $pager->getNavigationBar();
-                       $wgOut->addHTML( $navigation . $pager->getBody() . $navigation );
-               } else {
-                       $wgOut->addWikiMsg( 'specialpage-empty' );
-               }
-       }
-
-       protected function filterLinks() {
-               global $wgGroupPermissions, $wgUser;
-
-               // show/hide links
-               $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
-
-               // Option value -> message mapping
-               $filters = array(
-                       'hideliu' => 'rcshowhideliu',
-                       'hidepatrolled' => 'rcshowhidepatr',
-                       'hidebots' => 'rcshowhidebots'
-               );
-
-               // Disable some if needed
-               if ( $wgGroupPermissions['*']['createpage'] !== true )
-                       unset($filters['hideliu']);
-
-               if ( !$wgUser->useNPPatrol() )
-                       unset($filters['hidepatrolled']);
-
-               $links = array();
-               $changed = $this->opts->getChangedValues();
-               unset($changed['offset']); // Reset offset if query type changes
-
-               foreach ( $filters as $key => $msg ) {
-                       $onoff = 1 - $this->opts->getValue($key);
-                       $link = $this->skin->makeKnownLinkObj( $this->title, $showhide[$onoff],
-                               wfArrayToCGI( array( $key => $onoff ), $changed )
-                       );
-                       $links[$key] = wfMsgHtml( $msg, $link );
-               }
-
-               return implode( ' | ', $links );
-       }
-
-       protected function form() {
-               global $wgOut, $wgEnableNewpagesUserFilter, $wgScript;
-
-               // Consume values
-               $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
-               $namespace = $this->opts->consumeValue( 'namespace' );
-               $username = $this->opts->consumeValue( 'username' );
-
-               // Check username input validity
-               $ut = Title::makeTitleSafe( NS_USER, $username );
-               $userText = $ut ? $ut->getText() : '';
-
-               // Store query values in hidden fields so that form submission doesn't lose them
-               $hidden = array();
-               foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
-                       $hidden[] = Xml::hidden( $key, $value );
-               }
-               $hidden = implode( "\n", $hidden );
-
-               $form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) .
-                       Xml::hidden( 'title', $this->title->getPrefixedDBkey() ) .
-                       Xml::fieldset( wfMsg( 'newpages' ) ) .
-                       Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) .
-                       "<tr>
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                                       Xml::namespaceSelector( $namespace, 'all' ) .
-                               "</td>
-                       </tr>" .
-                       ($wgEnableNewpagesUserFilter ?
-                       "<tr>
-                               <td class='mw-label'>" .
-                                       Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) .
-                               "</td>
-                               <td class='mw-input'>" .
-                                       Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) .
-                               "</td>
-                       </tr>" : "" ) .
-                       "<tr> <td></td>
-                               <td class='mw-submit'>" .
-                                       Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
-                               "</td>
-                       </tr>" .
-                       "<tr>
-                               <td></td>
-                               <td class='mw-input'>" .
-                                       $this->filterLinks() .
-                               "</td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       $hidden .
-                       Xml::closeElement( 'form' );
-
-               $wgOut->addHTML( $form );
-       }
-
-       protected function setSyndicated() {
-               global $wgOut;
-               $queryParams = array(
-                       'namespace' => $this->opts->getValue( 'namespace' ),
-                       'username' => $this->opts->getValue( 'username' )
-               );
-               $wgOut->setSyndicated( true );
-               $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
-       }
-
-       /**
-        * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment
-        *
-        * @param $skin Skin to use
-        * @param $result Result row
-        * @return string
-        */
-       public function formatRow( $result ) {
-               global $wgLang, $wgContLang, $wgUser;
-               $dm = $wgContLang->getDirMark();
-
-               $title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title );
-               $time = $wgLang->timeAndDate( $result->rc_timestamp, true );
-               $plink = $this->skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rc_id : '' );
-               $hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
-               $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
-                       $wgLang->formatNum( $result->length ) );
-               $ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' .
-                       $this->skin->userToolLinks( $result->rc_user, $result->rc_user_text );
-               $comment = $this->skin->commentBlock( $result->rc_comment );
-               $css = $this->patrollable( $result ) ? " class='not-patrolled'" : '';
-
-               return "<li{$css}>{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}</li>\n";
-       }
-
-       /**
-        * Should a specific result row provide "patrollable" links?
-        *
-        * @param $result Result row
-        * @return bool
-        */
-       protected function patrollable( $result ) {
-               global $wgUser;
-               return ( $wgUser->useNPPatrol() && !$result->rc_patrolled );
-       }
-
-       /**
-        * Output a subscription feed listing recent edits to this page.
-        * @param string $type
-        */
-       protected function feed( $type ) {
-               require_once 'SpecialRecentchanges.php';
-
-               global $wgFeed, $wgFeedClasses;
-
-               if ( !$wgFeed ) {
-                       global $wgOut;
-                       $wgOut->addWikiMsg( 'feed-unavailable' );
-                       return;
-               }
-
-               if( !isset( $wgFeedClasses[$type] ) ) {
-                       global $wgOut;
-                       $wgOut->addWikiMsg( 'feed-invalid' );
-                       return;
-               }
-
-               $feed = new $wgFeedClasses[$type](
-                       $this->feedTitle(),
-                       wfMsg( 'tagline' ),
-                       $this->title->getFullUrl() );
-
-               $pager = new NewPagesPager( $this, $this->opts );
-               $limit = $this->opts->getValue( 'limit' );
-               global $wgFeedLimit;
-               if( $limit > $wgFeedLimit ) {
-                       $limit = $wgFeedLimit;
-               }
-               $pager->mLimit = $limit;
-
-               $feed->outHeader();
-               if( $pager->getNumRows() > 0 ) {
-                       while( $row = $pager->mResult->fetchObject() ) {
-                               $feed->outItem( $this->feedItem( $row ) );
-                       }
-               }
-               $feed->outFooter();
-       }
-
-       protected function feedTitle() {
-               global $wgContLanguageCode, $wgSitename;
-               $page = SpecialPage::getPage( 'Newpages' );
-               $desc = $page->getDescription();
-               return "$wgSitename - $desc [$wgContLanguageCode]";
-       }
-
-       protected function feedItem( $row ) {
-               $title = Title::MakeTitle( intval( $row->rc_namespace ), $row->rc_title );
-               if( $title ) {
-                       $date = $row->rc_timestamp;
-                       $comments = $title->getTalkPage()->getFullURL();
-
-                       return new FeedItem(
-                               $title->getPrefixedText(),
-                               $this->feedItemDesc( $row ),
-                               $title->getFullURL(),
-                               $date,
-                               $this->feedItemAuthor( $row ),
-                               $comments);
-               } else {
-                       return NULL;
-               }
-       }
-
-       /**
-        * Quickie hack... strip out wikilinks to more legible form from the comment.
-        */
-       protected function stripComment( $text ) {
-               return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
-       }
-
-       protected function feedItemAuthor( $row ) {
-               return isset( $row->rc_user_text ) ? $row->rc_user_text : '';
-       }
-
-       protected function feedItemDesc( $row ) {
-               $revision = Revision::newFromId( $row->rev_id );
-               if( $revision ) {
-                       return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' .
-                               htmlspecialchars( $revision->getComment() ) . 
-                               "</p>\n<hr />\n<div>" .
-                               nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
-               }
-               return '';
-       }
-}
-
-/**
- * @ingroup SpecialPage Pager
- */
-class NewPagesPager extends ReverseChronologicalPager {
-       // Stored opts
-       protected $opts, $mForm;
-
-       private $hideliu, $hidepatrolled, $hidebots, $namespace, $user, $spTitle;
-
-       function __construct( $form, FormOptions $opts ) {
-               parent::__construct();
-               $this->mForm = $form;
-               $this->opts = $opts;
-       }
-
-       function getTitle(){
-               static $title = null;
-               if ( $title === null )
-                       $title = SpecialPage::getTitleFor( 'Newpages' );
-               return $title;
-       }
-
-       function getQueryInfo() {
-               global $wgEnableNewpagesUserFilter, $wgGroupPermissions, $wgUser;
-               $conds = array();
-               $conds['rc_new'] = 1;
-
-               $namespace = $this->opts->getValue( 'namespace' );
-               $namespace = ( $namespace === 'all' ) ? false : intval( $namespace );
-
-               $username = $this->opts->getValue( 'username' );
-               $user = Title::makeTitleSafe( NS_USER, $username );
-
-               if( $namespace !== false ) {
-                       $conds['rc_namespace'] = $namespace;
-                       $rcIndexes = array( 'new_name_timestamp' );
-               } else {
-                       $rcIndexes = array( 'rc_timestamp' );
-               }
-               $conds[] = 'page_id = rc_cur_id';
-               $conds['page_is_redirect'] = 0;
-               # $wgEnableNewpagesUserFilter - temp WMF hack
-               if( $wgEnableNewpagesUserFilter && $user ) {
-                       $conds['rc_user_text'] = $user->getText();
-                       $rcIndexes = 'rc_user_text';
-               # If anons cannot make new pages, don't "exclude logged in users"!
-               } elseif( $wgGroupPermissions['*']['createpage'] && $this->opts->getValue( 'hideliu' ) ) {
-                       $conds['rc_user'] = 0;
-               }
-               # If this user cannot see patrolled edits or they are off, don't do dumb queries!
-               if( $this->opts->getValue( 'hidepatrolled' ) && $wgUser->useNPPatrol() ) {
-                       $conds['rc_patrolled'] = 0;
-               }
-               if( $this->opts->getValue( 'hidebots' ) ) {
-                       $conds['rc_bot'] = 0;
-               }
-
-               return array(
-                       'tables' => array( 'recentchanges', 'page' ),
-                       'fields' => 'rc_namespace,rc_title, rc_cur_id, rc_user,rc_user_text,rc_comment,
-                               rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id',
-                       'conds' => $conds,
-                       'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) )
-               );
-       }
-
-       function getIndexField() {
-               return 'rc_timestamp';
-       }
-
-       function formatRow( $row ) {
-               return $this->mForm->formatRow( $row );
-       }
-
-       function getStartBody() {
-               # Do a batch existence check on pages
-               $linkBatch = new LinkBatch();
-               while( $row = $this->mResult->fetchObject() ) {
-                       $linkBatch->add( NS_USER, $row->rc_user_text );
-                       $linkBatch->add( NS_USER_TALK, $row->rc_user_text );
-                       $linkBatch->add( $row->rc_namespace, $row->rc_title );
-               }
-               $linkBatch->execute();
-               return "<ul>";
-       }
-
-       function getEndBody() {
-               return "</ul>";
-       }
-}
index fe41c42..7b8748e 100644 (file)
@@ -642,7 +642,7 @@ class SpecialPage
                        $this->mFunction = $function;
                }
                if ( $file === 'default' ) {
-                       $this->mFile = dirname(__FILE__) . "/Special{$name}.php";
+                       $this->mFile = dirname(__FILE__) . "/specials/$name.php";
                } else {
                        $this->mFile = $file;
                }
diff --git a/includes/SpecialPopularpages.php b/includes/SpecialPopularpages.php
deleted file mode 100644 (file)
index eb57273..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * implements Special:Popularpages
- * @ingroup SpecialPage
- */
-class PopularPagesPage extends QueryPage {
-
-       function getName() {
-               return "Popularpages";
-       }
-
-       function isExpensive() {
-               # page_counter is not indexed
-               return true;
-       }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $page = $dbr->tableName( 'page' );
-
-               $query =
-                       "SELECT 'Popularpages' as type,
-                               page_namespace as namespace,
-                               page_title as title,
-                               page_counter as value
-                       FROM $page ";
-               $where =
-                       "WHERE page_is_redirect=0 AND page_namespace";
-
-               global $wgContentNamespaces;
-               if( empty( $wgContentNamespaces ) ) {
-                       $where .= '='.NS_MAIN;
-               } else if( count( $wgContentNamespaces ) > 1 ) {
-                       $where .= ' in (' . implode( ', ', $wgContentNamespaces ) . ')';
-               } else {
-                       $where .= '='.$wgContentNamespaces[0];
-               }
-
-               return $query . $where;
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-               $title = Title::makeTitle( $result->namespace, $result->title );
-               $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
-               $nv = wfMsgExt( 'nviews', array( 'parsemag', 'escape'),
-                       $wgLang->formatNum( $result->value ) );
-               return wfSpecialList($link, $nv);
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialPopularpages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $ppp = new PopularPagesPage();
-
-       return $ppp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php
deleted file mode 100644 (file)
index cdff571..0000000
+++ /dev/null
@@ -1,1125 +0,0 @@
-<?php
-/**
- * Hold things related to displaying and saving user preferences.
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point that create the "Preferences" object
- */
-function wfSpecialPreferences() {
-       global $wgRequest;
-
-       $form = new PreferencesForm( $wgRequest );
-       $form->execute();
-}
-
-/**
- * Preferences form handling
- * This object will show the preferences form and can save it as well.
- * @ingroup SpecialPage
- */
-class PreferencesForm {
-       var $mQuickbar, $mOldpass, $mNewpass, $mRetypePass, $mStubs;
-       var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
-       var $mUserLanguage, $mUserVariant;
-       var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
-       var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize;
-       var $mUnderline, $mWatchlistEdits;
-
-       /**
-        * Constructor
-        * Load some values
-        */
-       function PreferencesForm( &$request ) {
-               global $wgContLang, $wgUser, $wgAllowRealName;
-
-               $this->mQuickbar = $request->getVal( 'wpQuickbar' );
-               $this->mOldpass = $request->getVal( 'wpOldpass' );
-               $this->mNewpass = $request->getVal( 'wpNewpass' );
-               $this->mRetypePass =$request->getVal( 'wpRetypePass' );
-               $this->mStubs = $request->getVal( 'wpStubs' );
-               $this->mRows = $request->getVal( 'wpRows' );
-               $this->mCols = $request->getVal( 'wpCols' );
-               $this->mSkin = $request->getVal( 'wpSkin' );
-               $this->mMath = $request->getVal( 'wpMath' );
-               $this->mDate = $request->getVal( 'wpDate' );
-               $this->mUserEmail = $request->getVal( 'wpUserEmail' );
-               $this->mRealName = $wgAllowRealName ? $request->getVal( 'wpRealName' ) : '';
-               $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 0 : 1;
-               $this->mNick = $request->getVal( 'wpNick' );
-               $this->mUserLanguage = $request->getVal( 'wpUserLanguage' );
-               $this->mUserVariant = $request->getVal( 'wpUserVariant' );
-               $this->mSearch = $request->getVal( 'wpSearch' );
-               $this->mRecent = $request->getVal( 'wpRecent' );
-               $this->mRecentDays = $request->getVal( 'wpRecentDays' );
-               $this->mHourDiff = $request->getVal( 'wpHourDiff' );
-               $this->mSearchLines = $request->getVal( 'wpSearchLines' );
-               $this->mSearchChars = $request->getVal( 'wpSearchChars' );
-               $this->mImageSize = $request->getVal( 'wpImageSize' );
-               $this->mThumbSize = $request->getInt( 'wpThumbSize' );
-               $this->mUnderline = $request->getInt( 'wpOpunderline' );
-               $this->mAction = $request->getVal( 'action' );
-               $this->mReset = $request->getCheck( 'wpReset' );
-               $this->mPosted = $request->wasPosted();
-               $this->mSuccess = $request->getCheck( 'success' );
-               $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
-               $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
-               $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
-               $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
-
-               $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
-                       $this->mPosted &&
-                       $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
-
-               # User toggles  (the big ugly unsorted list of checkboxes)
-               $this->mToggles = array();
-               if ( $this->mPosted ) {
-                       $togs = User::getToggles();
-                       foreach ( $togs as $tname ) {
-                               $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0;
-                       }
-               }
-
-               $this->mUsedToggles = array();
-
-               # Search namespace options
-               # Note: namespaces don't necessarily have consecutive keys
-               $this->mSearchNs = array();
-               if ( $this->mPosted ) {
-                       $namespaces = $wgContLang->getNamespaces();
-                       foreach ( $namespaces as $i => $namespace ) {
-                               if ( $i >= 0 ) {
-                                       $this->mSearchNs[$i] = $request->getCheck( "wpNs$i" ) ? 1 : 0;
-                               }
-                       }
-               }
-
-               # Validate language
-               if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) {
-                       $this->mUserLanguage = 'nolanguage';
-               }
-
-               wfRunHooks( 'InitPreferencesForm', array( $this, $request ) );
-       }
-
-       function execute() {
-               global $wgUser, $wgOut;
-
-               if ( $wgUser->isAnon() ) {
-                       $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' );
-                       return;
-               }
-               if ( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-               if ( $this->mReset ) {
-                       $this->resetPrefs();
-                       $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) );
-               } else if ( $this->mSaveprefs ) {
-                       $this->savePreferences();
-               } else {
-                       $this->resetPrefs();
-                       $this->mainPrefsForm( '' );
-               }
-       }
-       /**
-        * @access private
-        */
-       function validateInt( &$val, $min=0, $max=0x7fffffff ) {
-               $val = intval($val);
-               $val = min($val, $max);
-               $val = max($val, $min);
-               return $val;
-       }
-
-       /**
-        * @access private
-        */
-       function validateFloat( &$val, $min, $max=0x7fffffff ) {
-               $val = floatval( $val );
-               $val = min( $val, $max );
-               $val = max( $val, $min );
-               return( $val );
-       }
-
-       /**
-        * @access private
-        */
-       function validateIntOrNull( &$val, $min=0, $max=0x7fffffff ) {
-               $val = trim($val);
-               if($val === '') {
-                       return null;
-               } else {
-                       return $this->validateInt( $val, $min, $max );
-               }
-       }
-
-       /**
-        * @access private
-        */
-       function validateDate( $val ) {
-               global $wgLang, $wgContLang;
-               if ( $val !== false && (
-                       in_array( $val, (array)$wgLang->getDatePreferences() ) ||
-                       in_array( $val, (array)$wgContLang->getDatePreferences() ) ) )
-               {
-                       return $val;
-               } else {
-                       return $wgLang->getDefaultDateFormat();
-               }
-       }
-
-       /**
-        * Used to validate the user inputed timezone before saving it as
-        * 'timecorrection', will return '00:00' if fed bogus data.
-        * Note: It's not a 100% correct implementation timezone-wise, it will
-        * accept stuff like '14:30',
-        * @access private
-        * @param string $s the user input
-        * @return string
-        */
-       function validateTimeZone( $s ) {
-               if ( $s !== '' ) {
-                       if ( strpos( $s, ':' ) ) {
-                               # HH:MM
-                               $array = explode( ':' , $s );
-                               $hour = intval( $array[0] );
-                               $minute = intval( $array[1] );
-                       } else {
-                               $minute = intval( $s * 60 );
-                               $hour = intval( $minute / 60 );
-                               $minute = abs( $minute ) % 60;
-                       }
-                       # Max is +14:00 and min is -12:00, see:
-                       # http://en.wikipedia.org/wiki/Timezone
-                       $hour = min( $hour, 14 );
-                       $hour = max( $hour, -12 );
-                       $minute = min( $minute, 59 );
-                       $minute = max( $minute, 0 );
-                       $s = sprintf( "%02d:%02d", $hour, $minute );
-               }
-               return $s;
-       }
-
-       /**
-        * @access private
-        */
-       function savePreferences() {
-               global $wgUser, $wgOut, $wgParser;
-               global $wgEnableUserEmail, $wgEnableEmail;
-               global $wgEmailAuthentication, $wgRCMaxAge;
-               global $wgAuth, $wgEmailConfirmToEdit;
-
-
-               if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
-                       if ( $this->mNewpass != $this->mRetypePass ) {
-                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'badretype' ) );
-                               $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) );
-                               return;
-                       }
-
-                       if (!$wgUser->checkPassword( $this->mOldpass )) {
-                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'wrongpassword' ) );
-                               $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
-                               return;
-                       }
-
-                       try {
-                               $wgUser->setPassword( $this->mNewpass );
-                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'success' ) );
-                               $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
-                       } catch( PasswordError $e ) {
-                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'error' ) );
-                               $this->mainPrefsForm( 'error', $e->getMessage() );
-                               return;
-                       }
-               }
-               $wgUser->setRealName( $this->mRealName );
-               $oldOptions = $wgUser->mOptions;
-
-               if( $wgUser->getOption( 'language' ) !== $this->mUserLanguage ) {
-                       $needRedirect = true;
-               } else {
-                       $needRedirect = false;
-               }
-
-               # Validate the signature and clean it up as needed
-               global $wgMaxSigChars;
-               if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
-                       global $wgLang;
-                       $this->mainPrefsForm( 'error',
-                               wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) );
-                       return;
-               } elseif( $this->mToggles['fancysig'] ) {
-                       if( $wgParser->validateSig( $this->mNick ) !== false ) {
-                               $this->mNick = $wgParser->cleanSig( $this->mNick );
-                       } else {
-                               $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) );
-                               return;
-                       }
-               } else {
-                       // When no fancy sig used, make sure ~{3,5} get removed.
-                       $this->mNick = $wgParser->cleanSigInSig( $this->mNick );
-               }
-
-               $wgUser->setOption( 'language', $this->mUserLanguage );
-               $wgUser->setOption( 'variant', $this->mUserVariant );
-               $wgUser->setOption( 'nickname', $this->mNick );
-               $wgUser->setOption( 'quickbar', $this->mQuickbar );
-               $wgUser->setOption( 'skin', $this->mSkin );
-               global $wgUseTeX;
-               if( $wgUseTeX ) {
-                       $wgUser->setOption( 'math', $this->mMath );
-               }
-               $wgUser->setOption( 'date', $this->validateDate( $this->mDate ) );
-               $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) );
-               $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) );
-               $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) );
-               $wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) );
-               $wgUser->setOption( 'rcdays', $this->validateInt($this->mRecentDays, 1, ceil($wgRCMaxAge / (3600*24))));
-               $wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) );
-               $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) );
-               $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) );
-               $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) );
-               $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mHourDiff, -12, 14 ) );
-               $wgUser->setOption( 'imagesize', $this->mImageSize );
-               $wgUser->setOption( 'thumbsize', $this->mThumbSize );
-               $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
-               $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
-               $wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch );
-               $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
-
-               # Set search namespace options
-               foreach( $this->mSearchNs as $i => $value ) {
-                       $wgUser->setOption( "searchNs{$i}", $value );
-               }
-
-               if( $wgEnableEmail && $wgEnableUserEmail ) {
-                       $wgUser->setOption( 'disablemail', $this->mEmailFlag );
-               }
-
-               # Set user toggles
-               foreach ( $this->mToggles as $tname => $tvalue ) {
-                       $wgUser->setOption( $tname, $tvalue );
-               }
-
-               $error = false;
-               if( $wgEnableEmail ) {
-                       $newadr = $this->mUserEmail;
-                       $oldadr = $wgUser->getEmail();
-                       if( ($newadr != '') && ($newadr != $oldadr) ) {
-                               # the user has supplied a new email address on the login page
-                               if( $wgUser->isValidEmailAddr( $newadr ) ) {
-                                       # new behaviour: set this new emailaddr from login-page into user database record
-                                       $wgUser->setEmail( $newadr );
-                                       # but flag as "dirty" = unauthenticated
-                                       $wgUser->invalidateEmail();
-                                       if ($wgEmailAuthentication) {
-                                               # Mail a temporary password to the dirty address.
-                                               # User can come back through the confirmation URL to re-enable email.
-                                               $result = $wgUser->sendConfirmationMail();
-                                               if( WikiError::isError( $result ) ) {
-                                                       $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
-                                               } else {
-                                                       $error = wfMsg( 'eauthentsent', $wgUser->getName() );
-                                               }
-                                       }
-                               } else {
-                                       $error = wfMsg( 'invalidemailaddress' );
-                               }
-                       } else {
-                               if( $wgEmailConfirmToEdit && empty( $newadr ) ) {
-                                       $this->mainPrefsForm( 'error', wfMsg( 'noemailtitle' ) );
-                                       return;
-                               }
-                               $wgUser->setEmail( $this->mUserEmail );
-                       }
-                       if( $oldadr != $newadr ) {
-                               wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) );
-                       }
-               }
-
-               if( !$wgAuth->updateExternalDB( $wgUser ) ){
-                       $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
-                       return;
-               }
-
-               $msg = '';
-               if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) {
-                       $this->mainPrefsForm( 'error', $msg );
-                       return;
-               }
-
-               $wgUser->setCookies();
-               $wgUser->saveSettings();
-
-               if( $needRedirect && $error === false ) {
-                       $title = SpecialPage::getTitleFor( 'Preferences' );
-                       $wgOut->redirect( $title->getFullURL( 'success' ) );
-                       return;
-               }
-
-               $wgOut->parserOptions( ParserOptions::newFromUser( $wgUser ) );
-               $this->mainPrefsForm( $error === false ? 'success' : 'error', $error);
-       }
-
-       /**
-        * @access private
-        */
-       function resetPrefs() {
-               global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName;
-
-               $this->mOldpass = $this->mNewpass = $this->mRetypePass = '';
-               $this->mUserEmail = $wgUser->getEmail();
-               $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
-               $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
-
-               # language value might be blank, default to content language
-               $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode );
-
-               $this->mUserVariant = $wgUser->getOption( 'variant');
-               $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0;
-               $this->mNick = $wgUser->getOption( 'nickname' );
-
-               $this->mQuickbar = $wgUser->getOption( 'quickbar' );
-               $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) );
-               $this->mMath = $wgUser->getOption( 'math' );
-               $this->mDate = $wgUser->getDatePreference();
-               $this->mRows = $wgUser->getOption( 'rows' );
-               $this->mCols = $wgUser->getOption( 'cols' );
-               $this->mStubs = $wgUser->getOption( 'stubthreshold' );
-               $this->mHourDiff = $wgUser->getOption( 'timecorrection' );
-               $this->mSearch = $wgUser->getOption( 'searchlimit' );
-               $this->mSearchLines = $wgUser->getOption( 'contextlines' );
-               $this->mSearchChars = $wgUser->getOption( 'contextchars' );
-               $this->mImageSize = $wgUser->getOption( 'imagesize' );
-               $this->mThumbSize = $wgUser->getOption( 'thumbsize' );
-               $this->mRecent = $wgUser->getOption( 'rclimit' );
-               $this->mRecentDays = $wgUser->getOption( 'rcdays' );
-               $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' );
-               $this->mUnderline = $wgUser->getOption( 'underline' );
-               $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
-               $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
-               $this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
-
-               $togs = User::getToggles();
-               foreach ( $togs as $tname ) {
-                       $this->mToggles[$tname] = $wgUser->getOption( $tname );
-               }
-
-               $namespaces = $wgContLang->getNamespaces();
-               foreach ( $namespaces as $i => $namespace ) {
-                       if ( $i >= NS_MAIN ) {
-                               $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i );
-                       }
-               }
-
-               wfRunHooks( 'ResetPreferences', array( $this, $wgUser ) );
-       }
-
-       /**
-        * @access private
-        */
-       function namespacesCheckboxes() {
-               global $wgContLang;
-
-               # Determine namespace checkboxes
-               $namespaces = $wgContLang->getNamespaces();
-               $r1 = null;
-
-               foreach ( $namespaces as $i => $name ) {
-                       if ($i < 0)
-                               continue;
-                       $checked = $this->mSearchNs[$i] ? "checked='checked'" : '';
-                       $name = str_replace( '_', ' ', $namespaces[$i] );
-
-                       if ( empty($name) )
-                               $name = wfMsg( 'blanknamespace' );
-
-                       $r1 .= "<input type='checkbox' value='1' name='wpNs$i' id='wpNs$i' {$checked}/> <label for='wpNs$i'>{$name}</label><br />\n";
-               }
-               return $r1;
-       }
-
-
-       function getToggle( $tname, $trailer = false, $disabled = false ) {
-               global $wgUser, $wgLang;
-
-               $this->mUsedToggles[$tname] = true;
-               $ttext = $wgLang->getUserToggle( $tname );
-
-               $checked = $wgUser->getOption( $tname ) == 1 ? ' checked="checked"' : '';
-               $disabled = $disabled ? ' disabled="disabled"' : '';
-               $trailer = $trailer ? $trailer : '';
-               return "<div class='toggle'><input type='checkbox' value='1' id=\"$tname\" name=\"wpOp$tname\"$checked$disabled />" .
-                       " <span class='toggletext'><label for=\"$tname\">$ttext</label>$trailer</span></div>\n";
-       }
-
-       function getToggles( $items ) {
-               $out = "";
-               foreach( $items as $item ) {
-                       if( $item === false )
-                               continue;
-                       if( is_array( $item ) ) {
-                               list( $key, $trailer ) = $item;
-                       } else {
-                               $key = $item;
-                               $trailer = false;
-                       }
-                       $out .= $this->getToggle( $key, $trailer );
-               }
-               return $out;
-       }
-
-       function addRow($td1, $td2) {
-               return "<tr><td class='mw-label'>$td1</td><td class='mw-input'>$td2</td></tr>";
-       }
-
-       /**
-        * Helper function for user information panel
-        * @param $td1 label for an item
-        * @param $td2 item or null
-        * @param $td3 optional help or null
-        * @return xhtml block
-        */
-       function tableRow( $td1, $td2 = null, $td3 = null ) {
-               global $wgContLang;
-
-               $align['align'] = $wgContLang->isRtl() ? 'right' : 'left';
-
-               if ( is_null( $td3 ) ) {
-                       $td3 = '';
-               } else {
-                       $td3 = Xml::tags( 'tr', null,
-                               Xml::tags( 'td', array( 'colspan' => '2' ), $td3 )
-                       );
-               }
-
-               if ( is_null( $td2 ) ) {
-                       $td1 = Xml::tags( 'td', $align + array( 'colspan' => '2' ), $td1 );
-                       $td2 = '';
-               } else {
-                       $td1 = Xml::tags( 'td', $align, $td1 );
-                       $td2 = Xml::tags( 'td', $align, $td2 );
-               }
-
-               return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n";
-
-       }
-
-       /**
-        * @access private
-        */
-       function mainPrefsForm( $status , $message = '' ) {
-               global $wgUser, $wgOut, $wgLang, $wgContLang;
-               global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
-               global $wgDisableLangConversion;
-               global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
-               global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
-               global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
-               global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
-               global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
-
-               $wgOut->setPageTitle( wfMsg( 'preferences' ) );
-               $wgOut->setArticleRelated( false );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->addScriptFile( 'prefs.js' );
-
-               $wgOut->disallowUserJs();  # Prevent hijacked user scripts from sniffing passwords etc.
-
-               if ( $this->mSuccess || 'success' == $status ) {
-                       $wgOut->wrapWikiMsg( '<div class="successbox"><strong>$1</strong></div>', 'savedprefs' );
-               } else  if ( 'error' == $status ) {
-                       $wgOut->addWikiText( '<div class="errorbox"><strong>' . $message  . '</strong></div>' );
-               } else if ( '' != $status ) {
-                       $wgOut->addWikiText( $message . "\n----" );
-               }
-
-               $qbs = $wgLang->getQuickbarSettings();
-               $skinNames = $wgLang->getSkinNames();
-               $mathopts = $wgLang->getMathNames();
-               $dateopts = $wgLang->getDatePreferences();
-               $togs = User::getToggles();
-
-               $titleObj = SpecialPage::getTitleFor( 'Preferences' );
-               $action = $titleObj->escapeLocalURL();
-
-               # Pre-expire some toggles so they won't show if disabled
-               $this->mUsedToggles[ 'shownumberswatching' ] = true;
-               $this->mUsedToggles[ 'showupdated' ] = true;
-               $this->mUsedToggles[ 'enotifwatchlistpages' ] = true;
-               $this->mUsedToggles[ 'enotifusertalkpages' ] = true;
-               $this->mUsedToggles[ 'enotifminoredits' ] = true;
-               $this->mUsedToggles[ 'enotifrevealaddr' ] = true;
-               $this->mUsedToggles[ 'ccmeonemails' ] = true;
-               $this->mUsedToggles[ 'uselivepreview' ] = true;
-
-
-               if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
-               else { $emfc = ''; }
-
-
-               if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
-                       if( $wgUser->getEmailAuthenticationTimestamp() ) {
-                               $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />';
-                               $disableEmailPrefs = false;
-                       } else {
-                               $disableEmailPrefs = true;
-                               $skin = $wgUser->getSkin();
-                               $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' .
-                                       $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ),
-                                               wfMsg( 'emailconfirmlink' ) ) . '<br />';
-                       }
-               } else {
-                       $emailauthenticated = '';
-                       $disableEmailPrefs = false;
-               }
-
-               if ($this->mUserEmail == '') {
-                       $emailauthenticated = wfMsg( 'noemailprefs' ) . '<br />';
-               }
-
-               $ps = $this->namespacesCheckboxes();
-
-               $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages', false, $disableEmailPrefs ) : '';
-               $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : '';
-               $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : '';
-               $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : '';
-
-               # </FIXME>
-
-               $wgOut->addHTML( "<form action=\"$action\" method='post'>" );
-               $wgOut->addHTML( "<div id='preferences'>" );
-
-               # User data
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'fieldset ' ) .
-                       Xml::element( 'legend', null, wfMsg('prefs-personal') ) .
-                       Xml::openElement( 'table' ) .
-                       $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) )
-               );
-
-               # Get groups to which the user belongs
-               $userEffectiveGroups = $wgUser->getEffectiveGroups();
-               $userEffectiveGroupsArray = array();
-               foreach( $userEffectiveGroups as $ueg ) {
-                       if( $ueg == '*' ) {
-                               // Skip the default * group, seems useless here
-                               continue;
-                       }
-                       $userEffectiveGroupsArray[] = User::makeGroupLinkHTML( $ueg );
-               }
-               asort( $userEffectiveGroupsArray );
-
-               $sk = $wgUser->getSkin();
-               $toolLinks = array();
-               $toolLinks[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'ListGroupRights' ), wfMsg( 'listgrouprights' ) );
-               # At the moment one tool link only but be prepared for the future...
-               # FIXME: Add a link to Special:Userrights for users who are allowed to use it. 
-               # $wgUser->isAllowed( 'userrights' ) seems to strict in some cases
-
-               $userInformationHtml =
-                       $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) .
-                       $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getId() ) ) .
-
-                       $this->tableRow(
-                               wfMsgExt( 'prefs-memberingroups', array( 'parseinline' ), count( $userEffectiveGroupsArray ) ),
-                               implode( wfMsg( 'comma-separator' ), $userEffectiveGroupsArray ) . 
-                               '<br />(' . implode( ' | ', $toolLinks ) . ')'
-                       ) .
-
-                       $this->tableRow(
-                               wfMsgHtml( 'prefs-edits' ),
-                               $wgLang->formatNum( User::edits( $wgUser->getId() ) )
-                       );
-
-               if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) {
-                       $wgOut->addHtml( $userInformationHtml );
-               }
-
-               if ( $wgAllowRealName ) {
-                       $wgOut->addHTML(
-                               $this->tableRow(
-                                       Xml::label( wfMsg('yourrealname'), 'wpRealName' ),
-                                       Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ),
-                                       Xml::tags('div', array( 'class' => 'prefsectiontip' ),
-                                               wfMsgExt( 'prefs-help-realname', 'parseinline' )
-                                       )
-                               )
-                       );
-               }
-               if ( $wgEnableEmail ) {
-                       $wgOut->addHTML(
-                               $this->tableRow(
-                                       Xml::label( wfMsg('youremail'), 'wpUserEmail' ),
-                                       Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ),
-                                       Xml::tags('div', array( 'class' => 'prefsectiontip' ),
-                                               wfMsgExt( $wgEmailConfirmToEdit ? 'prefs-help-email-required' : 'prefs-help-email', 'parseinline' )
-                                       )
-                               )
-                       );
-               }
-
-               global $wgParser, $wgMaxSigChars;
-               if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
-                       $invalidSig = $this->tableRow(
-                               '&nbsp;',
-                               Xml::element( 'span', array( 'class' => 'error' ),
-                                       wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) )
-                       );
-               } elseif( !empty( $this->mToggles['fancysig'] ) &&
-                       false === $wgParser->validateSig( $this->mNick ) ) {
-                       $invalidSig = $this->tableRow(
-                               '&nbsp;',
-                               Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) )
-                       );
-               } else {
-                       $invalidSig = '';
-               }
-
-               $wgOut->addHTML(
-                       $this->tableRow(
-                               Xml::label( wfMsg( 'yournick' ), 'wpNick' ),
-                               Xml::input( 'wpNick', 25, $this->mNick,
-                                       array(
-                                               'id' => 'wpNick',
-                                               // Note: $wgMaxSigChars is enforced in Unicode characters,
-                                               // both on the backend and now in the browser.
-                                               // Badly-behaved requests may still try to submit
-                                               // an overlong string, however.
-                                               'maxlength' => $wgMaxSigChars ) )
-                       ) .
-                       $invalidSig .
-                       $this->tableRow( '&nbsp;', $this->getToggle( 'fancysig' ) )
-               );
-
-               list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage );
-               $wgOut->addHTML(
-                       $this->tableRow( $lsLabel, $lsSelect )
-               );
-
-               /* see if there are multiple language variants to choose from*/
-               if(!$wgDisableLangConversion) {
-                       $variants = $wgContLang->getVariants();
-                       $variantArray = array();
-
-                       $languages = Language::getLanguageNames( true );
-                       foreach($variants as $v) {
-                               $v = str_replace( '_', '-', strtolower($v));
-                               if( array_key_exists( $v, $languages ) ) {
-                                       // If it doesn't have a name, we'll pretend it doesn't exist
-                                       $variantArray[$v] = $languages[$v];
-                               }
-                       }
-
-                       $options = "\n";
-                       foreach( $variantArray as $code => $name ) {
-                               $selected = ($code == $this->mUserVariant);
-                               $options .= Xml::option( "$code - $name", $code, $selected ) . "\n";
-                       }
-
-                       if(count($variantArray) > 1) {
-                               $wgOut->addHtml(
-                                       $this->tableRow(
-                                               Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ),
-                                               Xml::tags( 'select',
-                                                       array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ),
-                                                       $options
-                                               )
-                                       )
-                               );
-                       }
-               }
-
-               # Password
-               if( $wgAuth->allowPasswordChange() ) {
-                       $wgOut->addHTML(
-                               $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) .
-                               $this->tableRow(
-                                       Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ),
-                                       Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) )
-                               ) .
-                               $this->tableRow(
-                                       Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ),
-                                       Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) )
-                               ) .
-                               $this->tableRow(
-                                       Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ),
-                                       Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) )
-                               ) .
-                               Xml::tags( 'tr', null,
-                                       Xml::tags( 'td', array( 'colspan' => '2' ),
-                                               $this->getToggle( "rememberpassword" )
-                                       )
-                               )
-                       );
-               }
-
-               # <FIXME>
-               # Enotif
-               if ( $wgEnableEmail ) {
-
-                       $moreEmail = '';
-                       if ($wgEnableUserEmail) {
-                               // fixme -- the "allowemail" pseudotoggle is a hacked-together
-                               // inversion for the "disableemail" preference.
-                               $emf = wfMsg( 'allowemail' );
-                               $disabled = $disableEmailPrefs ? ' disabled="disabled"' : '';
-                               $moreEmail =
-                                       "<input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label>" .
-                                       $this->getToggle( 'ccmeonemails', '', $disableEmailPrefs );
-                       }
-
-
-                       $wgOut->addHTML(
-                               $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) .
-                               $this->tableRow(
-                                       $emailauthenticated.
-                                       $enotifrevealaddr.
-                                       $enotifwatchlistpages.
-                                       $enotifusertalkpages.
-                                       $enotifminoredits.
-                                       $moreEmail
-                               )
-                       );
-               }
-               # </FIXME>
-
-               $wgOut->addHTML(
-                       Xml::closeElement( 'table' ) .
-                       Xml::closeElement( 'fieldset' )
-               );
-
-
-               # Quickbar
-               #
-               if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') {
-                       $wgOut->addHtml( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" );
-                       for ( $i = 0; $i < count( $qbs ); ++$i ) {
-                               if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; }
-                               else { $checked = ""; }
-                               $wgOut->addHTML( "<div><label><input type='radio' name='wpQuickbar' value=\"$i\"$checked />{$qbs[$i]}</label></div>\n" );
-                       }
-                       $wgOut->addHtml( "</fieldset>\n\n" );
-               } else {
-                       # Need to output a hidden option even if the relevant skin is not in use,
-                       # otherwise the preference will get reset to 0 on submit
-                       $wgOut->addHtml( wfHidden( 'wpQuickbar', $this->mQuickbar ) );
-               }
-
-               # Skin
-               #
-               $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg('skin') . "</legend>\n" );
-               $mptitle = Title::newMainPage();
-               $previewtext = wfMsg('skinpreview');
-               # Only show members of Skin::getSkinNames() rather than
-               # $skinNames (skins is all skin names from Language.php)
-               $validSkinNames = Skin::getSkinNames();
-               # Sort by UI skin name. First though need to update validSkinNames as sometimes
-               # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
-               foreach ($validSkinNames as $skinkey => & $skinname ) {
-                       if ( isset( $skinNames[$skinkey] ) )  {
-                               $skinname = $skinNames[$skinkey];
-                       }
-               }
-               asort($validSkinNames);
-               foreach ($validSkinNames as $skinkey => $sn ) {
-                       if ( in_array( $skinkey, $wgSkipSkins ) ) {
-                               continue;
-                       }
-                       $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
-
-                       $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey"));
-                       $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
-                       if( $skinkey == $wgDefaultSkin )
-                               $sn .= ' (' . wfMsg( 'default' ) . ')';
-                       $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" );
-               }
-               $wgOut->addHTML( "</fieldset>\n\n" );
-
-               # Math
-               #
-               global $wgUseTeX;
-               if( $wgUseTeX ) {
-                       $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('math') . '</legend>' );
-                       foreach ( $mathopts as $k => $v ) {
-                               $checked = ($k == $this->mMath);
-                               $wgOut->addHTML(
-                                       Xml::openElement( 'div' ) .
-                                       Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) .
-                                       Xml::closeElement( 'div' ) . "\n"
-                               );
-                       }
-                       $wgOut->addHTML( "</fieldset>\n\n" );
-               }
-
-               # Files
-               #
-               $wgOut->addHTML(
-                       "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n"
-               );
-
-               $imageLimitOptions = null;
-               foreach ( $wgImageLimits as $index => $limits ) {
-                       $selected = ($index == $this->mImageSize);
-                       $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" .
-                               wfMsg('unit-pixel'), $index, $selected );
-               }
-
-               $imageSizeId = 'wpImageSize';
-               $wgOut->addHTML(
-                       "<div>" . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " .
-                       Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) .
-                               $imageLimitOptions .
-                       Xml::closeElement( 'select' ) . "</div>\n"
-               );
-
-               $imageThumbOptions = null;
-               foreach ( $wgThumbLimits as $index => $size ) {
-                       $selected = ($index == $this->mThumbSize);
-                       $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index,
-                               $selected);
-               }
-
-               $thumbSizeId = 'wpThumbSize';
-               $wgOut->addHTML(
-                       "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " .
-                       Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
-                               $imageThumbOptions .
-                       Xml::closeElement( 'select' ) . "</div>\n"
-               );
-
-               $wgOut->addHTML( "</fieldset>\n\n" );
-
-               # Date format
-               #
-               # Date/Time
-               #
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'datetime' ) ) . "\n"
-               );
-
-               if ($dateopts) {
-                       $wgOut->addHTML(
-                               Xml::openElement( 'fieldset' ) .
-                               Xml::element( 'legend', null, wfMsg( 'dateformat' ) ) . "\n"
-                       );
-                       $idCnt = 0;
-                       $epoch = '20010115161234'; # Wikipedia day
-                       foreach( $dateopts as $key ) {
-                               if( $key == 'default' ) {
-                                       $formatted = wfMsg( 'datedefault' );
-                               } else {
-                                       $formatted = $wgLang->timeanddate( $epoch, false, $key );
-                               }
-                               $wgOut->addHTML(
-                                       Xml::tags( 'div', null,
-                                               Xml::radioLabel( $formatted, 'wpDate', $key, "wpDate$idCnt", $key == $this->mDate )
-                                       ) . "\n"
-                               );
-                               $idCnt++;
-                       }
-                       $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" );
-               }
-
-               $nowlocal = $wgLang->time( $now = wfTimestampNow(), true );
-               $nowserver = $wgLang->time( $now, false );
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) .
-                       Xml::openElement( 'table' ) .
-                       $this->addRow( wfMsg( 'servertime' ), $nowserver ) .
-                       $this->addRow( wfMsg( 'localtime' ), $nowlocal ) .
-                       $this->addRow(
-                               Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff'  ),
-                               Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) .
-                       "<tr>
-                               <td></td>
-                               <td class='mw-submit'>" .
-                                       Xml::element( 'input',
-                                               array( 'type' => 'button',
-                                                       'value' => wfMsg( 'guesstimezone' ),
-                                                       'onclick' => 'javascript:guessTimezone()',
-                                                       'id' => 'guesstimezonebutton',
-                                                       'style' => 'display:none;' ) ) .
-                               "</td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::tags( 'div', array( 'class' => 'prefsectiontip' ), wfMsgExt( 'timezonetext', 'parseinline' ) ).
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'fieldset' ) . "\n\n"
-               );
-
-               # Editing
-               #
-               global $wgLivePreview;
-               $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'textboxsize' ) . '</legend>
-                       <div>' .
-                               wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) .
-                               ' ' .
-                               wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
-                       "</div>" .
-                       $this->getToggles( array(
-                               'editsection',
-                               'editsectiononrightclick',
-                               'editondblclick',
-                               'editwidth',
-                               'showtoolbar',
-                               'previewonfirst',
-                               'previewontop',
-                               'minordefault',
-                               'externaleditor',
-                               'externaldiff',
-                               $wgLivePreview ? 'uselivepreview' : false,
-                               'forceeditsummary',
-                       ) ) . '</fieldset>'
-               );
-
-               # Recent changes
-               $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-rc' ) . '</legend>' );
-
-               $rc  = '<table><tr>';
-               $rc .= '<td>' . Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . '</td>';
-               $rc .= '<td>' . Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . '</td>';
-               $rc .= '</tr><tr>';
-               $rc .= '<td>' . Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . '</td>';
-               $rc .= '<td>' . Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . '</td>';
-               $rc .= '</tr></table>';
-               $wgOut->addHtml( $rc );
-
-               $wgOut->addHtml( '<br />' );
-
-               $toggles[] = 'hideminor';
-               if( $wgRCShowWatchingUsers )
-                       $toggles[] = 'shownumberswatching';
-               $toggles[] = 'usenewrc';
-               $wgOut->addHtml( $this->getToggles( $toggles ) );
-
-               $wgOut->addHtml( '</fieldset>' );
-
-               # Watchlist
-               $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' );
-
-               $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) );
-               $wgOut->addHtml( '<br /><br />' );
-
-               $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) );
-               $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) );
-               $wgOut->addHtml( '<br /><br />' );
-
-               $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) );
-
-               if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) )
-                       $wgOut->addHtml( $this->getToggle( 'watchcreations' ) );
-               foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) {
-                       if( $wgUser->isAllowed( $action ) )
-                               $wgOut->addHtml( $this->getToggle( $toggle ) );
-               }
-               $this->mUsedToggles['watchcreations'] = true;
-               $this->mUsedToggles['watchdefault'] = true;
-               $this->mUsedToggles['watchmoves'] = true;
-               $this->mUsedToggles['watchdeletion'] = true;
-
-               $wgOut->addHtml( '</fieldset>' );
-
-               # Search
-               $ajaxsearch = $wgAjaxSearch ?
-                       $this->addRow(
-                               Xml::label( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
-                               Xml::check( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
-                       ) : '';
-               $mwsuggest = $wgEnableMWSuggest ?
-                       $this->addRow(
-                               Xml::label( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ),
-                               Xml::check( 'wpDisableMWSuggest', $this->mDisableMWSuggest, array( 'id' => 'wpDisableMWSuggest' ) )
-                       ) : '';
-               $wgOut->addHTML(
-                       // Elements for the search tab itself
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'searchresultshead' ) ) .
-                       // Elements for the search options in the search tab
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'prefs-searchoptions' ) ) .
-                       Xml::openElement( 'table' ) .
-                       $ajaxsearch .
-                       $this->addRow(
-                               Xml::label( wfMsg( 'resultsperpage' ), 'wpSearch' ),
-                               Xml::input( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
-                       ) .
-                       $this->addRow(
-                               Xml::label( wfMsg( 'contextlines' ), 'wpSearchLines' ),
-                               Xml::input( 'wpSearchLines', 4, $this->mSearchLines, array( 'id' => 'wpSearchLines' ) )
-                       ) .
-                       $this->addRow(
-                               Xml::label( wfMsg( 'contextchars' ), 'wpSearchChars' ),
-                               Xml::input( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) )
-                       ) .
-                       $mwsuggest .
-                       Xml::closeElement( 'table' ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       // Elements for the namespace options in the search tab
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'prefs-namespaces' ) ) .
-                       wfMsgExt( 'defaultns', array( 'parse' ) ) .
-                       $ps .
-                       Xml::closeElement( 'fieldset' ) .
-                       // End of the search tab
-                       Xml::closeElement( 'fieldset' )
-               );
-
-               # Misc
-               #
-               $wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>');
-               $wgOut->addHtml( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label>&nbsp;' );
-               $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) );
-               $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) );
-               $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) );
-               $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) );
-               $msgUnderlinedefault = htmlspecialchars( wfMsg ( 'underline-default' ) );
-               $uopt = $wgUser->getOption("underline");
-               $s0 = $uopt == 0 ? ' selected="selected"' : '';
-               $s1 = $uopt == 1 ? ' selected="selected"' : '';
-               $s2 = $uopt == 2 ? ' selected="selected"' : '';
-               $wgOut->addHTML("
-<div class='toggle'><p><label for='wpOpunderline'>$msgUnderline</label>
-<select name='wpOpunderline' id='wpOpunderline'>
-<option value=\"0\"$s0>$msgUnderlinenever</option>
-<option value=\"1\"$s1>$msgUnderlinealways</option>
-<option value=\"2\"$s2>$msgUnderlinedefault</option>
-</select></p></div>");
-
-               foreach ( $togs as $tname ) {
-                       if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
-                               $wgOut->addHTML( $this->getToggle( $tname ) );
-                       }
-               }
-               $wgOut->addHTML( '</fieldset>' );
-
-               wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
-
-               $token = htmlspecialchars( $wgUser->editToken() );
-               $skin = $wgUser->getSkin();
-               $wgOut->addHTML( "
-       <div id='prefsubmit'>
-       <div>
-               <input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) . '"'.$skin->tooltipAndAccesskey('save')." />
-               <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" />
-       </div>
-
-       </div>
-
-       <input type='hidden' name='wpEditToken' value=\"{$token}\" />
-       </div></form>\n" );
-
-               $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ),
-                       wfMsgExt( 'clearyourcache', 'parseinline' ) )
-               );
-       }
-}
diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php
deleted file mode 100644 (file)
index 1819d4e..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point : initialise variables and call subfunctions.
- * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default NULL)
- * @param $specialPage SpecialPage object.
- */
-function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
-       global $wgRequest, $wgOut, $wgContLang;
-
-       # GET values
-       $from = $wgRequest->getVal( 'from' );
-       $prefix = $wgRequest->getVal( 'prefix' );
-       $namespace = $wgRequest->getInt( 'namespace' );
-       $namespaces = $wgContLang->getNamespaces();
-
-       $indexPage = new SpecialPrefixIndex();
-
-       $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) )
-               ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) )
-               : wfMsg( 'allarticles' )
-       );
-
-       if ( isset($par) ) {
-               $indexPage->showChunk( $namespace, $par, $specialPage->including(), $from );
-       } elseif ( isset($prefix) ) {
-               $indexPage->showChunk( $namespace, $prefix, $specialPage->including(), $from );
-       } elseif ( isset($from) ) {
-               $indexPage->showChunk( $namespace, $from, $specialPage->including(), $from );
-       } else {
-               $wgOut->addHtml($indexPage->namespaceForm ( $namespace, null ));
-       }
-}
-
-/**
- * implements Special:Prefixindex
- * @ingroup SpecialPage
- */
-class SpecialPrefixindex extends SpecialAllpages {
-       // Inherit $maxPerPage
-
-       // Define other properties
-       protected $name = 'Prefixindex';
-       protected $nsfromMsg = 'allpagesprefix';
-
-       /**
-        * @param integer $namespace (Default NS_MAIN)
-        * @param string $from list all pages from this name (default FALSE)
-        */
-       function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = null ) {
-               global $wgOut, $wgUser, $wgContLang;
-
-               $fname = 'indexShowChunk';
-
-               $sk = $wgUser->getSkin();
-
-               if (!isset($from)) $from = $prefix;
-
-               $fromList = $this->getNamespaceKeyAndText($namespace, $from);
-               $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
-               $namespaces = $wgContLang->getNamespaces();
-               $align = $wgContLang->isRtl() ? 'left' : 'right';
-
-               if ( !$prefixList || !$fromList ) {
-                       $out = wfMsgWikiHtml( 'allpagesbadtitle' );
-               } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
-                       // Show errormessage and reset to NS_MAIN
-                       $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
-                       $namespace = NS_MAIN;
-               } else {
-                       list( $namespace, $prefixKey, $prefix ) = $prefixList;
-                       list( /* $fromNs */, $fromKey, $from ) = $fromList;
-
-                       ### FIXME: should complain if $fromNs != $namespace
-
-                       $dbr = wfGetDB( DB_SLAVE );
-
-                       $res = $dbr->select( 'page',
-                               array( 'page_namespace', 'page_title', 'page_is_redirect' ),
-                               array(
-                                       'page_namespace' => $namespace,
-                                       'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
-                                       'page_title >= ' . $dbr->addQuotes( $fromKey ),
-                               ),
-                               $fname,
-                               array(
-                                       'ORDER BY'  => 'page_title',
-                                       'LIMIT'     => $this->maxPerPage + 1,
-                                       'USE INDEX' => 'name_title',
-                               )
-                       );
-
-                       ### FIXME: side link to previous
-
-                       $n = 0;
-                       if( $res->numRows() > 0 ) {
-                               $out = '<table style="background: inherit;" border="0" width="100%">';
-       
-                               while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
-                                       $t = Title::makeTitle( $s->page_namespace, $s->page_title );
-                                       if( $t ) {
-                                               $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
-                                                       $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
-                                                       ($s->page_is_redirect ? '</div>' : '' );
-                                       } else {
-                                               $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
-                                       }
-                                       if( $n % 3 == 0 ) {
-                                               $out .= '<tr>';
-                                       }
-                                       $out .= "<td>$link</td>";
-                                       $n++;
-                                       if( $n % 3 == 0 ) {
-                                               $out .= '</tr>';
-                                       }
-                               }
-                               if( ($n % 3) != 0 ) {
-                                       $out .= '</tr>';
-                               }
-                               $out .= '</table>';
-                       } else {
-                               $out = '';
-                       }
-               }
-
-               if ( $including ) {
-                       $out2 = '';
-               } else {
-                       $nsForm = $this->namespaceForm ( $namespace, $prefix );
-                       $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
-                       $out2 .= '<tr valign="top"><td>' . $nsForm;
-                       $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
-                                       $sk->makeKnownLink( $wgContLang->specialPage( $this->name ),
-                                               wfMsg ( 'allpages' ) );
-                       if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
-                               $namespaceparam = $namespace ? "&namespace=$namespace" : "";
-                               $out2 .= " | " . $sk->makeKnownLink(
-                                       $wgContLang->specialPage( $this->name ),
-                                       wfMsg ( 'nextpage', $s->page_title ),
-                                       "from=" . wfUrlEncode ( $s->page_title ) .
-                                       "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam );
-                       }
-                       $out2 .= "</td></tr></table><hr />";
-               }
-
-               $wgOut->addHtml( $out2 . $out );
-       }
-}
diff --git a/includes/SpecialProtectedpages.php b/includes/SpecialProtectedpages.php
deleted file mode 100644 (file)
index e168902..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @todo document
- * @ingroup SpecialPage
- */
-class ProtectedPagesForm {
-
-       protected $IdLevel = 'level';
-       protected $IdType  = 'type';
-
-       public function showList( $msg = '' ) {
-               global $wgOut, $wgRequest;
-
-               $wgOut->setPagetitle( wfMsg( "protectedpages" ) );
-               if ( "" != $msg ) {
-                       $wgOut->setSubtitle( $msg );
-               }
-
-               // Purge expired entries on one in every 10 queries
-               if ( !mt_rand( 0, 10 ) ) {
-                       Title::purgeExpiredRestrictions();
-               }
-
-               $type = $wgRequest->getVal( $this->IdType );
-               $level = $wgRequest->getVal( $this->IdLevel );
-               $sizetype = $wgRequest->getVal( 'sizetype' );
-               $size = $wgRequest->getIntOrNull( 'size' );
-               $NS = $wgRequest->getIntOrNull( 'namespace' );
-               $indefOnly = $wgRequest->getBool( 'indefonly' ) ? 1 : 0;
-
-               $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly );
-
-               $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly ) );
-
-               if ( $pager->getNumRows() ) {
-                       $s = $pager->getNavigationBar();
-                       $s .= "<ul>" .
-                               $pager->getBody() .
-                               "</ul>";
-                       $s .= $pager->getNavigationBar();
-               } else {
-                       $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>';
-               }
-               $wgOut->addHTML( $s );
-       }
-
-       /**
-        * Callback function to output a restriction
-        * @param $row object Protected title
-        * @return string Formatted <li> element
-        */
-       public function formatRow( $row ) {
-               global $wgUser, $wgLang, $wgContLang;
-
-               wfProfileIn( __METHOD__ );
-
-               static $skin=null;
-
-               if( is_null( $skin ) )
-                       $skin = $wgUser->getSkin();
-
-               $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
-               $link = $skin->makeLinkObj( $title );
-
-               $description_items = array ();
-
-               $protType = wfMsgHtml( 'restriction-level-' . $row->pr_level );
-
-               $description_items[] = $protType;
-
-               if ( $row->pr_cascade ) {
-                       $description_items[] = wfMsg( 'protect-summary-cascade' );
-               }
-
-               $expiry_description = '';
-               $stxt = '';
-
-               if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) {
-                       $expiry = Block::decodeExpiry( $row->pr_expiry );
-
-                       $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
-
-                       $description_items[] = $expiry_description;
-               }
-
-               if (!is_null($size = $row->page_len)) {
-                       if ($size == 0)
-                               $stxt = ' <small>' . wfMsgHtml('historyempty') . '</small>';
-                       else
-                               $stxt = ' <small>' . wfMsgHtml('historysize', $wgLang->formatNum( $size ) ) . '</small>';
-                       $stxt = $wgContLang->getDirMark() . $stxt;
-               }
-
-               # Show a link to the change protection form for allowed users otherwise a link to the protection log
-               if( $wgUser->isAllowed( 'protect' ) ) {
-                       $changeProtection = ' (' . $skin->makeKnownLinkObj( $title, wfMsgHtml( 'protect_change' ), 'action=unprotect' ) . ')';
-               } else {
-                       $ltitle = SpecialPage::getTitleFor( 'Log' );
-                       $changeProtection = ' (' . $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'protectlogpage' ), 'type=protect&page=' . $title->getPrefixedUrl() ) . ')';
-               }
-
-               wfProfileOut( __METHOD__ );
-
-               return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "</li>\n";
-       }
-
-       /**
-        * @param $namespace int
-        * @param $type string
-        * @param $level string
-        * @param $minsize int
-        * @param $indefOnly bool
-        * @return string Input form
-        * @private
-        */
-       protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly ) {
-               global $wgScript;
-               $title = SpecialPage::getTitleFor( 'ProtectedPages' );
-               return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
-                       Xml::hidden( 'title', $title->getPrefixedDBkey() ) . "&nbsp;\n" .
-                       $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
-                       $this->getTypeMenu( $type ) . "&nbsp;\n" .
-                       $this->getLevelMenu( $level ) . "&nbsp;\n" .
-                       "<br /><span style='white-space: nowrap'>&nbsp;&nbsp;" .
-                       $this->getExpiryCheck( $indefOnly ) . "&nbsp;\n" .
-                       $this->getSizeLimit( $sizetype, $size ) . "&nbsp;\n" .
-                       "</span>" .
-                       "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' );
-       }
-
-       /**
-        * Prepare the namespace filter drop-down; standard namespace
-        * selector, sans the MediaWiki namespace
-        *
-        * @param mixed $namespace Pre-select namespace
-        * @return string
-        */
-       protected function getNamespaceMenu( $namespace = null ) {
-               return "<span style='white-space: nowrap'>" .
-                       Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;'
-                       . Xml::namespaceSelector( $namespace, '' ) . "</span>";
-       }
-
-       /**
-        * @return string Formatted HTML
-        */
-       protected function getExpiryCheck( $indefOnly ) {
-               return
-                       Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n";
-       }
-
-       /**
-        * @return string Formatted HTML
-        */
-       protected function getSizeLimit( $sizetype, $size ) {
-               $max = $sizetype === 'max';
-
-               return
-                       Xml::radioLabel( wfMsg('minimum-size'), 'sizetype', 'min', 'wpmin', !$max ) .
-                       '&nbsp;' .
-                       Xml::radioLabel( wfMsg('maximum-size'), 'sizetype', 'max', 'wpmax', $max ) .
-                       '&nbsp;' .
-                       Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) .
-                       '&nbsp;' .
-                       Xml::label( wfMsg('pagesize'), 'wpsize' );
-       }
-
-       /**
-        * @return string Formatted HTML
-        */
-       protected function getTypeMenu( $pr_type ) {
-               global $wgRestrictionTypes;
-
-               $m = array(); // Temporary array
-               $options = array();
-
-               // First pass to load the log names
-               foreach( $wgRestrictionTypes as $type ) {
-                       $text = wfMsg("restriction-$type");
-                       $m[$text] = $type;
-               }
-
-               // Third pass generates sorted XHTML content
-               foreach( $m as $text => $type ) {
-                       $selected = ($type == $pr_type );
-                       $options[] = Xml::option( $text, $type, $selected ) . "\n";
-               }
-
-               return "<span style='white-space: nowrap'>" .
-                       Xml::label( wfMsg('restriction-type') , $this->IdType ) . '&nbsp;' .
-                       Xml::tags( 'select',
-                               array( 'id' => $this->IdType, 'name' => $this->IdType ),
-                               implode( "\n", $options ) ) . "</span>";
-       }
-
-       /**
-        * @return string Formatted HTML
-        */
-       protected function getLevelMenu( $pr_level ) {
-               global $wgRestrictionLevels;
-
-               $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
-               $options = array();
-
-               // First pass to load the log names
-               foreach( $wgRestrictionLevels as $type ) {
-                       if ( $type !='' && $type !='*') {
-                               $text = wfMsg("restriction-level-$type");
-                               $m[$text] = $type;
-                       }
-               }
-
-               // Third pass generates sorted XHTML content
-               foreach( $m as $text => $type ) {
-                       $selected = ($type == $pr_level );
-                       $options[] = Xml::option( $text, $type, $selected );
-               }
-
-               return
-                       Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
-                       Xml::tags( 'select',
-                               array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
-                               implode( "\n", $options ) );
-       }
-}
-
-/**
- * @todo document
- * @ingroup Pager
- */
-class ProtectedPagesPager extends AlphabeticPager {
-       public $mForm, $mConds;
-       private $type, $level, $namespace, $sizetype, $size, $indefonly;
-
-       function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0, $indefonly=false ) {
-               $this->mForm = $form;
-               $this->mConds = $conds;
-               $this->type = ( $type ) ? $type : 'edit';
-               $this->level = $level;
-               $this->namespace = $namespace;
-               $this->sizetype = $sizetype;
-               $this->size = intval($size);
-               $this->indefonly = (bool)$indefonly;
-               parent::__construct();
-       }
-
-       function getStartBody() {
-               wfProfileIn( __METHOD__ );
-               # Do a link batch query
-               $lb = new LinkBatch;
-               while( $row = $this->mResult->fetchObject() ) {
-                       $lb->add( $row->page_namespace, $row->page_title );
-               }
-               $lb->execute();
-
-               wfProfileOut( __METHOD__ );
-               return '';
-       }
-
-       function formatRow( $row ) {
-               return $this->mForm->formatRow( $row );
-       }
-
-       function getQueryInfo() {
-               $conds = $this->mConds;
-               $conds[] = 'pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
-               $conds[] = 'page_id=pr_page';
-               $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type );
-
-               if( $this->sizetype=='min' ) {
-                       $conds[] = 'page_len>=' . $this->size;
-               } else if( $this->sizetype=='max' ) {
-                       $conds[] = 'page_len<=' . $this->size;
-               }
-
-               if( $this->indefonly ) {
-                       $conds[] = "pr_expiry = 'infinity' OR pr_expiry IS NULL";
-               }
-
-               if( $this->level )
-                       $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
-               if( !is_null($this->namespace) )
-                       $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
-               return array(
-                       'tables' => array( 'page_restrictions', 'page' ),
-                       'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade',
-                       'conds' => $conds
-               );
-       }
-
-       function getIndexField() {
-               return 'pr_id';
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialProtectedpages() {
-
-       $ppForm = new ProtectedPagesForm();
-
-       $ppForm->showList();
-}
diff --git a/includes/SpecialProtectedtitles.php b/includes/SpecialProtectedtitles.php
deleted file mode 100644 (file)
index 2ec68a6..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @todo document
- * @ingroup SpecialPage
- */
-class ProtectedTitlesForm {
-
-       protected $IdLevel = 'level';
-       protected $IdType  = 'type';
-
-       function showList( $msg = '' ) {
-               global $wgOut, $wgRequest;
-
-               $wgOut->setPagetitle( wfMsg( "protectedtitles" ) );
-               if ( "" != $msg ) {
-                       $wgOut->setSubtitle( $msg );
-               }
-
-               // Purge expired entries on one in every 10 queries
-               if ( !mt_rand( 0, 10 ) ) {
-                       Title::purgeExpiredRestrictions();
-               }
-
-               $type = $wgRequest->getVal( $this->IdType );
-               $level = $wgRequest->getVal( $this->IdLevel );
-               $sizetype = $wgRequest->getVal( 'sizetype' );
-               $size = $wgRequest->getIntOrNull( 'size' );
-               $NS = $wgRequest->getIntOrNull( 'namespace' );
-
-               $pager = new ProtectedTitlesPager( $this, array(), $type, $level, $NS, $sizetype, $size );
-
-               $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) );
-
-               if ( $pager->getNumRows() ) {
-                       $s = $pager->getNavigationBar();
-                       $s .= "<ul>" .
-                               $pager->getBody() .
-                               "</ul>";
-                       $s .= $pager->getNavigationBar();
-               } else {
-                       $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>';
-               }
-               $wgOut->addHTML( $s );
-       }
-
-       /**
-        * Callback function to output a restriction
-        */
-       function formatRow( $row ) {
-               global $wgUser, $wgLang, $wgContLang;
-
-               wfProfileIn( __METHOD__ );
-
-               static $skin=null;
-
-               if( is_null( $skin ) )
-                       $skin = $wgUser->getSkin();
-
-               $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
-               $link = $skin->makeLinkObj( $title );
-
-               $description_items = array ();
-
-               $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm );
-
-               $description_items[] = $protType;
-
-               $expiry_description = ''; $stxt = '';
-
-               if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) {
-                       $expiry = Block::decodeExpiry( $row->pt_expiry );
-
-                       $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
-
-                       $description_items[] = $expiry_description;
-               }
-
-               wfProfileOut( __METHOD__ );
-
-               return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . "</li>\n";
-       }
-
-       /**
-        * @param $namespace int
-        * @param $type string
-        * @param $level string
-        * @param $minsize int
-        * @private
-        */
-       function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
-               global $wgScript;
-               $action = htmlspecialchars( $wgScript );
-               $title = SpecialPage::getTitleFor( 'ProtectedTitles' );
-               $special = htmlspecialchars( $title->getPrefixedDBkey() );
-               return "<form action=\"$action\" method=\"get\">\n" .
-                       '<fieldset>' .
-                       Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
-                       Xml::hidden( 'title', $special ) . "&nbsp;\n" .
-                       $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
-                       // $this->getLevelMenu( $level ) . "<br/>\n" .
-                       "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
-                       "</fieldset></form>";
-       }
-
-       /**
-        * Prepare the namespace filter drop-down; standard namespace
-        * selector, sans the MediaWiki namespace
-        *
-        * @param mixed $namespace Pre-select namespace
-        * @return string
-        */
-       function getNamespaceMenu( $namespace = null ) {
-               return Xml::label( wfMsg( 'namespace' ), 'namespace' )
-                       . '&nbsp;'
-                       . Xml::namespaceSelector( $namespace, '' );
-       }
-
-       /**
-        * @return string Formatted HTML
-        * @private
-        */
-       function getLevelMenu( $pr_level ) {
-               global $wgRestrictionLevels;
-
-               $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
-               $options = array();
-
-               // First pass to load the log names
-               foreach( $wgRestrictionLevels as $type ) {
-                       if ( $type !='' && $type !='*') {
-                               $text = wfMsg("restriction-level-$type");
-                               $m[$text] = $type;
-                       }
-               }
-
-               // Third pass generates sorted XHTML content
-               foreach( $m as $text => $type ) {
-                       $selected = ($type == $pr_level );
-                       $options[] = Xml::option( $text, $type, $selected );
-               }
-
-               return
-                       Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
-                       Xml::tags( 'select',
-                               array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
-                               implode( "\n", $options ) );
-       }
-}
-
-/**
- * @todo document
- * @ingroup Pager
- */
-class ProtectedtitlesPager extends AlphabeticPager {
-       public $mForm, $mConds;
-
-       function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
-               $this->mForm = $form;
-               $this->mConds = $conds;
-               $this->level = $level;
-               $this->namespace = $namespace;
-               $this->size = intval($size);
-               parent::__construct();
-       }
-
-       function getStartBody() {
-               wfProfileIn( __METHOD__ );
-               # Do a link batch query
-               $this->mResult->seek( 0 );
-               $lb = new LinkBatch;
-
-               while ( $row = $this->mResult->fetchObject() ) {
-                       $lb->add( $row->pt_namespace, $row->pt_title );
-               }
-
-               $lb->execute();
-               wfProfileOut( __METHOD__ );
-               return '';
-       }
-
-       function formatRow( $row ) {
-               return $this->mForm->formatRow( $row );
-       }
-
-       function getQueryInfo() {
-               $conds = $this->mConds;
-               $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
-
-               if( !is_null($this->namespace) )
-                       $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
-               return array(
-                       'tables' => 'protected_titles',
-                       'fields' => 'pt_namespace,pt_title,pt_create_perm,pt_expiry,pt_timestamp',
-                       'conds' => $conds
-               );
-       }
-
-       function getIndexField() {
-               return 'pt_timestamp';
-       }
-}
-
-/**
- * Constructor
- */
-function wfSpecialProtectedtitles() {
-
-       $ppForm = new ProtectedTitlesForm();
-
-       $ppForm->showList();
-}
diff --git a/includes/SpecialRandompage.php b/includes/SpecialRandompage.php
deleted file mode 100644 (file)
index 0e7ada1..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-<?php
-
-/**
- * Special page to direct the user to a random page
- *
- * @ingroup SpecialPage
- * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
- * @license GNU General Public Licence 2.0 or later
- */
-class RandomPage extends SpecialPage {
-       private $namespace = NS_MAIN;  // namespace to select pages from
-
-       function __construct( $name = 'Randompage' ){
-               parent::__construct( $name );
-       }
-
-       public function getNamespace() {
-               return $this->namespace;
-       }
-
-       public function setNamespace ( $ns ) {
-               if( $ns < NS_MAIN ) $ns = NS_MAIN;
-               $this->namespace = $ns;
-       }
-
-       // select redirects instead of normal pages?
-       // Overriden by SpecialRandomredirect
-       public function isRedirect(){
-               return false;
-       }
-
-       public function execute( $par ) {
-               global $wgOut, $wgContLang;
-
-               if ($par)
-                       $this->setNamespace( $wgContLang->getNsIndex( $par ) );
-
-               $title = $this->getRandomTitle();
-
-               if( is_null( $title ) ) {
-                       $this->setHeaders();
-                       $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' );
-                       return;
-               }
-
-               $query = $this->isRedirect() ? 'redirect=no' : '';
-               $wgOut->redirect( $title->getFullUrl( $query ) );
-       }
-
-
-       /**
-        * Choose a random title.
-        * @return Title object (or null if nothing to choose from)
-        */
-       public function getRandomTitle() {
-               $randstr = wfRandom();
-               $row = $this->selectRandomPageFromDB( $randstr );
-
-               /* If we picked a value that was higher than any in
-                * the DB, wrap around and select the page with the
-                * lowest value instead!  One might think this would
-                * skew the distribution, but in fact it won't cause
-                * any more bias than what the page_random scheme
-                * causes anyway.  Trust me, I'm a mathematician. :)
-                */
-               if( !$row )
-                       $row = $this->selectRandomPageFromDB( "0" );
-
-               if( $row )
-                       return Title::makeTitleSafe( $this->namespace, $row->page_title );
-               else
-                       return null;
-       }
-
-       private function selectRandomPageFromDB( $randstr ) {
-               global $wgExtraRandompageSQL;
-               $fname = 'RandomPage::selectRandomPageFromDB';
-
-               $dbr = wfGetDB( DB_SLAVE );
-
-               $use_index = $dbr->useIndexClause( 'page_random' );
-               $page = $dbr->tableName( 'page' );
-
-               $ns = (int) $this->namespace;
-               $redirect = $this->isRedirect() ? 1 : 0;
-
-               $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
-               $sql = "SELECT page_title
-                       FROM $page $use_index
-                       WHERE page_namespace = $ns
-                       AND page_is_redirect = $redirect
-                       AND page_random >= $randstr
-                       $extra
-                       ORDER BY page_random";
-
-               $sql = $dbr->limitResult( $sql, 1, 0 );
-               $res = $dbr->query( $sql, $fname );
-               return $dbr->fetchObject( $res );
-       }
-}
diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php
deleted file mode 100644 (file)
index 629d5b3..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * Special page to direct the user to a random redirect page (minus the second redirect)
- *
- * @ingroup SpecialPage
- * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
- * @license GNU General Public Licence 2.0 or later
- */
-class SpecialRandomredirect extends RandomPage {
-       function __construct(){
-               parent::__construct( 'Randomredirect' );
-       }
-
-       // Override parent::isRedirect()
-       public function isRedirect(){
-               return true;
-       }
-}
diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php
deleted file mode 100644 (file)
index cd1dac0..0000000
+++ /dev/null
@@ -1,803 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-require_once( dirname(__FILE__) . '/ChangesList.php' );
-
-/**
- * Constructor
- */
-function wfSpecialRecentchanges( $par, $specialPage ) {
-       global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
-       global $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
-       global $wgAllowCategorizedRecentChanges ;
-
-       # Get query parameters
-       $feedFormat = $wgRequest->getVal( 'feed' );
-
-       /* Checkbox values can't be true by default, because
-        * we cannot differentiate between unset and not set at all
-        */
-       $defaults = array(
-       /* int  */ 'days' => $wgUser->getDefaultOption('rcdays'),
-       /* int  */ 'limit' => $wgUser->getDefaultOption('rclimit'),
-       /* bool */ 'hideminor' => false,
-       /* bool */ 'hidebots' => true,
-       /* bool */ 'hideanons' => false,
-       /* bool */ 'hideliu' => false,
-       /* bool */ 'hidepatrolled' => false,
-       /* bool */ 'hidemyself' => false,
-       /* text */ 'from' => '',
-       /* text */ 'namespace' => null,
-       /* bool */ 'invert' => false,
-       /* bool */ 'categories_any' => false,
-       );
-
-       extract($defaults);
-
-
-       $days = $wgUser->getOption( 'rcdays', $defaults['days']);
-       $days = $wgRequest->getInt( 'days', $days );
-
-       $limit = $wgUser->getOption( 'rclimit', $defaults['limit'] );
-
-       #       list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' );
-       $limit = $wgRequest->getInt( 'limit', $limit );
-
-       /* order of selection: url > preferences > default */
-       $hideminor = $wgRequest->getBool( 'hideminor', $wgUser->getOption( 'hideminor') ? true : $defaults['hideminor'] );
-
-       # As a feed, use limited settings only
-       if( $feedFormat ) {
-               global $wgFeedLimit;
-               $limit = min( $wgFeedLimit, $limit );
-       } else {
-
-               $namespace = $wgRequest->getIntOrNull( 'namespace' );
-               $invert = $wgRequest->getBool( 'invert', $defaults['invert'] );
-               $hidebots = $wgRequest->getBool( 'hidebots', $defaults['hidebots'] );
-               $hideanons = $wgRequest->getBool( 'hideanons', $defaults['hideanons'] );
-               $hideliu = $wgRequest->getBool( 'hideliu', $defaults['hideliu'] );
-               $hidepatrolled = $wgRequest->getBool( 'hidepatrolled', $defaults['hidepatrolled'] );
-               $hidemyself = $wgRequest->getBool ( 'hidemyself', $defaults['hidemyself'] );
-               $from = $wgRequest->getVal( 'from', $defaults['from'] );
-
-               # Get query parameters from path
-               if( $par ) {
-                       $bits = preg_split( '/\s*,\s*/', trim( $par ) );
-                       foreach ( $bits as $bit ) {
-                               if ( 'hidebots' == $bit ) $hidebots = 1;
-                               if ( 'bots' == $bit ) $hidebots = 0;
-                               if ( 'hideminor' == $bit ) $hideminor = 1;
-                               if ( 'minor' == $bit ) $hideminor = 0;
-                               if ( 'hideliu' == $bit ) $hideliu = 1;
-                               if ( 'hidepatrolled' == $bit ) $hidepatrolled = 1;
-                               if ( 'hideanons' == $bit ) $hideanons = 1;
-                               if ( 'hidemyself' == $bit ) $hidemyself = 1;
-
-                               if ( is_numeric( $bit ) ) {
-                                       $limit = $bit;
-                               }
-
-                               $m = array();
-                               if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
-                                       $limit = $m[1];
-                               }
-
-                               if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
-                                       $days = $m[1];
-                               }
-                       }
-               }
-       }
-
-       if ( $limit < 0 || $limit > 5000 ) $limit = $defaults['limit'];
-
-       # Database connection and caching
-       $dbr = wfGetDB( DB_SLAVE );
-
-       $cutoff_unixtime = time() - ( $days * 86400 );
-       $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
-       $cutoff = $dbr->timestamp( $cutoff_unixtime );
-       if(preg_match('/^[0-9]{14}$/', $from) and $from > wfTimestamp(TS_MW,$cutoff)) {
-               $cutoff = $dbr->timestamp($from);
-       } else {
-               $from = $defaults['from'];
-       }
-
-       # 10 seconds server-side caching max
-       $wgOut->setSquidMaxage( 10 );
-
-       # Get last modified date, for client caching
-       # Don't use this if we are using the patrol feature, patrol changes don't update the timestamp
-       $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ );
-       if ( $feedFormat || !$wgUseRCPatrol ) {
-               if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){
-                       # Client cache fresh and headers sent, nothing more to do.
-                       return;
-               }
-       }
-
-       # It makes no sense to hide both anons and logged-in users
-       # Where this occurs, force anons to be shown
-       $forcebot = false;
-       if( $hideanons && $hideliu ){
-               # Check if the user wants to show bots only
-               if( $hidebots ){
-                       $hideanons = 0;
-               } else {
-                       $forcebot = true;
-                       $hidebots = 0;
-               }
-       }
-
-       # Form WHERE fragments for all the options
-       $hidem  = $hideminor ? 'AND rc_minor = 0' : '';
-       $hidem .= $hidebots ? ' AND rc_bot = 0' : '';
-       $hidem .= $hideliu && !$forcebot ? ' AND rc_user = 0' : '';
-       $hidem .= ($wgUser->useRCPatrol() && $hidepatrolled ) ? ' AND rc_patrolled = 0' : '';
-       $hidem .= $hideanons && !$forcebot ? ' AND rc_user != 0' : '';
-       $hidem .= $forcebot ? ' AND rc_bot = 1' : '';
-
-       if( $hidemyself ) {
-               if( $wgUser->getId() ) {
-                       $hidem .= ' AND rc_user != ' . $wgUser->getId();
-               } else {
-                       $hidem .= ' AND rc_user_text != ' . $dbr->addQuotes( $wgUser->getName() );
-               }
-       }
-
-       // JOIN on watchlist for users
-       $uid = $wgUser->getId();
-       if( $uid ) {
-               $tables = array( 'recentchanges', 'watchlist' );
-               $join_conds = array( 'watchlist' => array('LEFT JOIN',"wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") );
-       } else {
-               $tables = array( 'recentchanges' );
-               $join_conds = array();
-       }
-       
-       # Namespace filtering
-       $hidem .= is_null($namespace) ?  '' : ' AND rc_namespace' . ($invert ? '!=' : '=') . $namespace;
-       
-       // Is there either one namespace selected or excluded?
-       // Also, if this is "all" or main namespace, just use timestamp index.
-       if( is_null($namespace) || $invert || $namespace == NS_MAIN ) {
-               $res = $dbr->select( $tables, '*',
-                       array( "rc_timestamp >= '{$cutoff}' {$hidem}" ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, 
-                               'USE INDEX' => array('recentchanges' => 'rc_timestamp') ),
-                       $join_conds );
-       // We have a new_namespace_time index! UNION over new=(0,1) and sort result set!
-       } else {
-               // New pages
-               $sqlNew = $dbr->selectSQLText( $tables, '*',
-                       array( 'rc_new' => 1,
-                               "rc_timestamp >= '{$cutoff}' {$hidem}" ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, 
-                               'USE INDEX' =>  array('recentchanges' => 'new_name_timestamp') ),
-                       $join_conds );
-               // Old pages
-               $sqlOld = $dbr->selectSQLText( $tables, '*',
-                       array( 'rc_new' => 0,
-                               "rc_timestamp >= '{$cutoff}' {$hidem}" ),
-                       __METHOD__,
-                       array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, 
-                               'USE INDEX' =>  array('recentchanges' => 'new_name_timestamp') ),
-                       $join_conds );
-               # Join the two fast queries, and sort the result set
-               $sql = "($sqlNew) UNION ($sqlOld) ORDER BY rc_timestamp DESC LIMIT $limit";
-               $res = $dbr->query( $sql, __METHOD__ );
-       }
-       
-       // Fetch results, prepare a batch link existence check query
-       $rows = array();
-       $batch = new LinkBatch;
-       while( $row = $dbr->fetchObject( $res ) ){
-               $rows[] = $row;
-               if ( !$feedFormat ) {
-                       // User page and talk links
-                       $batch->add( NS_USER, $row->rc_user_text  );
-                       $batch->add( NS_USER_TALK, $row->rc_user_text  );
-               }
-
-       }
-       $dbr->freeResult( $res );
-
-       if( $feedFormat ) {
-               rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod );
-       } else {
-
-               # Web output...
-
-               // Run existence checks
-               $batch->execute();
-               $any = $wgRequest->getBool( 'categories_any', $defaults['categories_any']);
-
-               // Output header
-               if ( !$specialPage->including() ) {
-                       $wgOut->addWikiText( wfMsgForContentNoTrans( "recentchangestext" ) );
-
-                       // Dump everything here
-                       $nondefaults = array();
-
-                       wfAppendToArrayIfNotDefault( 'days', $days, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'limit', $limit , $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'hideminor', $hideminor, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'hidebots', $hidebots, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'hideanons', $hideanons, $defaults, $nondefaults );
-                       wfAppendToArrayIfNotDefault( 'hideliu', $hideliu, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'hidepatrolled', $hidepatrolled, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'hidemyself', $hidemyself, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'from', $from, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'namespace', $namespace, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'invert', $invert, $defaults, $nondefaults);
-                       wfAppendToArrayIfNotDefault( 'categories_any', $any, $defaults, $nondefaults);
-
-                       // Add end of the texts
-                       $wgOut->addHTML( '<div class="rcoptions">' . rcOptionsPanel( $defaults, $nondefaults ) . "\n" );
-                       $wgOut->addHTML( rcNamespaceForm( $namespace, $invert, $nondefaults, $any ) . '</div>'."\n");
-               }
-
-               // And now for the content
-               $wgOut->setSyndicated( true );
-
-               $list = ChangesList::newFromUser( $wgUser );
-
-               if ( $wgAllowCategorizedRecentChanges ) {
-                       $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
-                       $categories = str_replace ( "|" , "\n" , $categories ) ;
-                       $categories = explode ( "\n" , $categories ) ;
-                       rcFilterByCategories ( $rows , $categories , $any ) ;
-               }
-
-               $s = $list->beginRecentChangesList();
-               $counter = 1;
-
-               $showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' );
-               $watcherCache = array();
-
-               foreach( $rows as $obj ){
-                       if( $limit == 0) {
-                               break;
-                       }
-
-                       if ( ! ( $hideminor     && $obj->rc_minor     ) &&
-                            ! ( $hidepatrolled && $obj->rc_patrolled ) ) {
-                               $rc = RecentChange::newFromRow( $obj );
-                               $rc->counter = $counter++;
-
-                               if ($wgShowUpdatedMarker
-                                       && !empty( $obj->wl_notificationtimestamp )
-                                       && ($obj->rc_timestamp >= $obj->wl_notificationtimestamp)) {
-                                               $rc->notificationtimestamp = true;
-                               } else {
-                                       $rc->notificationtimestamp = false;
-                               }
-
-                               $rc->numberofWatchingusers = 0; // Default
-                               if ($showWatcherCount && $obj->rc_namespace >= 0) {
-                                       if (!isset($watcherCache[$obj->rc_namespace][$obj->rc_title])) {
-                                               $watcherCache[$obj->rc_namespace][$obj->rc_title] =
-                                                       $dbr->selectField( 'watchlist',
-                                                               'COUNT(*)',
-                                                               array(
-                                                                       'wl_namespace' => $obj->rc_namespace,
-                                                                       'wl_title' => $obj->rc_title,
-                                                               ),
-                                                               __METHOD__ . '-watchers' );
-                                       }
-                                       $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
-                               }
-                               $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
-                               --$limit;
-                       }
-               }
-               $s .= $list->endRecentChangesList();
-               $wgOut->addHTML( $s );
-       }
-}
-
-function rcFilterByCategories ( &$rows , $categories , $any ) {
-       if( empty( $categories ) ) {
-               return;
-       }
-
-       # Filter categories
-       $cats = array () ;
-       foreach ( $categories AS $cat ) {
-               $cat = trim ( $cat ) ;
-               if ( $cat == "" ) continue ;
-               $cats[] = $cat ;
-       }
-
-       # Filter articles
-       $articles = array () ;
-       $a2r = array () ;
-       foreach ( $rows AS $k => $r ) {
-               $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
-               $id = $nt->getArticleID() ;
-               if ( $id == 0 ) continue ; # Page might have been deleted...
-               if ( !in_array ( $id , $articles ) ) {
-                       $articles[] = $id ;
-               }
-               if ( !isset ( $a2r[$id] ) ) {
-                       $a2r[$id] = array() ;
-               }
-               $a2r[$id][] = $k ;
-       }
-
-       # Shortcut?
-       if ( count ( $articles ) == 0 OR count ( $cats ) == 0 )
-               return ;
-
-       # Look up
-       $c = new Categoryfinder ;
-       $c->seed ( $articles , $cats , $any ? "OR" : "AND" ) ;
-       $match = $c->run () ;
-
-       # Filter
-       $newrows = array () ;
-       foreach ( $match AS $id ) {
-               foreach ( $a2r[$id] AS $rev ) {
-                       $k = $rev ;
-                       $newrows[$k] = $rows[$k] ;
-               }
-       }
-       $rows = $newrows ;
-}
-
-function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) {
-       global $messageMemc, $wgFeedCacheTimeout;
-       global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode;
-       global $wgFeed;
-
-       if ( !$wgFeed ) {
-               global $wgOut;
-               $wgOut->addWikiMsg( 'feed-unavailable' );
-               return;
-       }
-
-       if( !isset( $wgFeedClasses[$feedFormat] ) ) {
-               wfHttpError( 500, "Internal Server Error", "Unsupported feed type." );
-               return false;
-       }
-
-       $timekey = wfMemcKey( 'rcfeed', $feedFormat, 'timestamp' );
-       $key = wfMemcKey( 'rcfeed', $feedFormat, 'limit', $limit, 'minor', $hideminor );
-
-       $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchanges' ) .
-               ' [' . $wgContLanguageCode . ']';
-       $feed = new $wgFeedClasses[$feedFormat](
-               $feedTitle,
-               htmlspecialchars( wfMsgForContent( 'recentchanges-feed-description' ) ),
-               $wgTitle->getFullUrl() );
-
-       //purge cache if requested
-       global $wgRequest, $wgUser;
-       $purge = $wgRequest->getVal( 'action' ) == 'purge';
-       if ( $purge && $wgUser->isAllowed('purge') ) {
-               $messageMemc->delete( $timekey );
-               $messageMemc->delete( $key );
-       }
-
-       /**
-        * Bumping around loading up diffs can be pretty slow, so where
-        * possible we want to cache the feed output so the next visitor
-        * gets it quick too.
-        */
-       $cachedFeed = false;
-       if( ( $wgFeedCacheTimeout > 0 ) && ( $feedLastmod = $messageMemc->get( $timekey ) ) ) {
-               /**
-                * If the cached feed was rendered very recently, we may
-                * go ahead and use it even if there have been edits made
-                * since it was rendered. This keeps a swarm of requests
-                * from being too bad on a super-frequently edited wiki.
-                */
-               if( time() - wfTimestamp( TS_UNIX, $feedLastmod )
-                               < $wgFeedCacheTimeout
-                       || wfTimestamp( TS_UNIX, $feedLastmod )
-                               > wfTimestamp( TS_UNIX, $lastmod ) ) {
-                       wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
-                       $cachedFeed = $messageMemc->get( $key );
-               } else {
-                       wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
-               }
-       }
-       if( is_string( $cachedFeed ) ) {
-               wfDebug( "RC: Outputting cached feed\n" );
-               $feed->httpHeaders();
-               echo $cachedFeed;
-       } else {
-               wfDebug( "RC: rendering new feed and caching it\n" );
-               ob_start();
-               rcDoOutputFeed( $rows, $feed );
-               $cachedFeed = ob_get_contents();
-               ob_end_flush();
-
-               $expire = 3600 * 24; # One day
-               $messageMemc->set( $key, $cachedFeed );
-               $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
-       }
-       return true;
-}
-
-/**
- * @todo document
- * @param $rows Database resource with recentchanges rows
- */
-function rcDoOutputFeed( $rows, &$feed ) {
-       wfProfileIn( __METHOD__ );
-
-       $feed->outHeader();
-
-       # Merge adjacent edits by one user
-       $sorted = array();
-       $n = 0;
-       foreach( $rows as $obj ) {
-               if( $n > 0 &&
-                       $obj->rc_namespace >= 0 &&
-                       $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id &&
-                       $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) {
-                       $sorted[$n-1]->rc_last_oldid = $obj->rc_last_oldid;
-               } else {
-                       $sorted[$n] = $obj;
-                       $n++;
-               }
-       }
-
-       foreach( $sorted as $obj ) {
-               $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
-               $talkpage = $title->getTalkPage();
-               $item = new FeedItem(
-                       $title->getPrefixedText(),
-                       rcFormatDiff( $obj ),
-                       $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ),
-                       $obj->rc_timestamp,
-                       ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text,
-                       $talkpage->getFullURL()
-                       );
-               $feed->outItem( $item );
-       }
-       $feed->outFooter();
-       wfProfileOut( __METHOD__ );
-}
-
-/**
- *
- */
-function rcCountLink( $lim, $d, $page='Recentchanges', $more='', $active = false ) {
-       global $wgUser, $wgLang, $wgContLang;
-       $sk = $wgUser->getSkin();
-       $s = $sk->makeKnownLink( $wgContLang->specialPage( $page ),
-         ($lim ? $wgLang->formatNum( "{$lim}" ) : wfMsg( 'recentchangesall' ) ), "{$more}" .
-         ($d ? "days={$d}&" : '') . 'limit='.$lim, '', '',
-         $active ? 'style="font-weight: bold;"' : '' );
-       return $s;
-}
-
-/**
- *
- */
-function rcDaysLink( $lim, $d, $page='Recentchanges', $more='', $active = false ) {
-       global $wgUser, $wgLang, $wgContLang;
-       $sk = $wgUser->getSkin();
-       $s = $sk->makeKnownLink( $wgContLang->specialPage( $page ),
-         ($d ? $wgLang->formatNum( "{$d}" ) : wfMsg( 'recentchangesall' ) ), $more.'days='.$d .
-         ($lim ? '&limit='.$lim : ''), '', '',
-         $active ? 'style="font-weight: bold;"' : '' );
-       return $s;
-}
-
-/**
- * Used by Recentchangeslinked
- */
-function rcDayLimitLinks( $days, $limit, $page='Recentchanges', $more='', $doall = false, $minorLink = '',
-       $botLink = '', $liuLink = '', $patrLink = '', $myselfLink = '' ) {
-       global $wgRCLinkLimits, $wgRCLinkDays;
-       if ($more != '') $more .= '&';
-       
-       # Sort data for display and make sure it's unique after we've added user data.
-       # FIXME: why does this piss around with globals like this? Why is $limit added on globally?
-       $wgRCLinkLimits[] = $limit;
-       $wgRCLinkDays[] = $days;
-       sort($wgRCLinkLimits);
-       sort($wgRCLinkDays);
-       $wgRCLinkLimits = array_unique($wgRCLinkLimits);
-       $wgRCLinkDays = array_unique($wgRCLinkDays);
-       
-       $cl = array();
-       foreach( $wgRCLinkLimits as $countLink ) {
-               $cl[] = rcCountLink( $countLink, $days, $page, $more, $countLink == $limit );
-       }
-       if( $doall ) $cl[] = rcCountLink( 0, $days, $page, $more );
-       $cl = implode( ' | ', $cl);
-       
-       $dl = array();
-       foreach( $wgRCLinkDays as $daysLink ) {
-               $dl[] = rcDaysLink( $limit, $daysLink, $page, $more, $daysLink == $days );
-       }
-       if( $doall ) $dl[] = rcDaysLink( $limit, 0, $page, $more );
-       $dl = implode( ' | ', $dl);
-       
-       $linkParts = array( 'minorLink' => 'minor', 'botLink' => 'bots', 'liuLink' => 'liu', 'patrLink' => 'patr', 'myselfLink' => 'mine' );
-       foreach( $linkParts as $linkVar => $linkMsg ) {
-               if( $$linkVar != '' )
-                       $links[] = wfMsgHtml( 'rcshowhide' . $linkMsg, $$linkVar );
-       }
-
-       $shm = implode( ' | ', $links );
-       $note = wfMsg( 'rclinks', $cl, $dl, $shm );
-       return $note;
-}
-
-
-/**
- * Makes change an option link which carries all the other options
- * @param $title see Title
- * @param $override
- * @param $options
- */
-function makeOptionsLink( $title, $override, $options, $active = false ) {
-       global $wgUser, $wgContLang;
-       $sk = $wgUser->getSkin();
-       return $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchanges' ),
-               htmlspecialchars( $title ), wfArrayToCGI( $override, $options ), '', '',
-               $active ? 'style="font-weight: bold;"' : '' );
-}
-
-/**
- * Creates the options panel.
- * @param $defaults
- * @param $nondefaults
- */
-function rcOptionsPanel( $defaults, $nondefaults ) {
-       global $wgLang, $wgUser, $wgRCLinkLimits, $wgRCLinkDays;
-
-       $options = $nondefaults + $defaults;
-
-       if( $options['from'] )
-               $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
-                       $wgLang->formatNum( $options['limit'] ),
-                       $wgLang->timeanddate( $options['from'], true ) );
-       else
-               $note = wfMsgExt( 'rcnote', array( 'parseinline' ),
-                       $wgLang->formatNum( $options['limit'] ),
-                       $wgLang->formatNum( $options['days'] ),
-                       $wgLang->timeAndDate( wfTimestampNow(), true ) );
-
-       # Sort data for display and make sure it's unique after we've added user data.
-       $wgRCLinkLimits[] = $options['limit'];
-       $wgRCLinkDays[] = $options['days'];
-       sort($wgRCLinkLimits);
-       sort($wgRCLinkDays);
-       $wgRCLinkLimits = array_unique($wgRCLinkLimits);
-       $wgRCLinkDays = array_unique($wgRCLinkDays);
-       
-       // limit links
-       foreach( $wgRCLinkLimits as $value ) {
-               $cl[] = makeOptionsLink( $wgLang->formatNum( $value ),
-                       array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ) ;
-       }
-       $cl = implode( ' | ', $cl);
-
-       // day links, reset 'from' to none
-       foreach( $wgRCLinkDays as $value ) {
-               $dl[] = makeOptionsLink( $wgLang->formatNum( $value ),
-                       array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ) ;
-       }
-       $dl = implode( ' | ', $dl);
-
-
-       // show/hide links
-       $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ));
-       $minorLink = makeOptionsLink( $showhide[1-$options['hideminor']],
-               array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults);
-       $botLink = makeOptionsLink( $showhide[1-$options['hidebots']],
-               array( 'hidebots' => 1-$options['hidebots'] ), $nondefaults);
-       $anonsLink = makeOptionsLink( $showhide[ 1 - $options['hideanons'] ],
-               array( 'hideanons' => 1 - $options['hideanons'] ), $nondefaults );
-       $liuLink   = makeOptionsLink( $showhide[1-$options['hideliu']],
-               array( 'hideliu' => 1-$options['hideliu'] ), $nondefaults);
-       $patrLink  = makeOptionsLink( $showhide[1-$options['hidepatrolled']],
-               array( 'hidepatrolled' => 1-$options['hidepatrolled'] ), $nondefaults);
-       $myselfLink = makeOptionsLink( $showhide[1-$options['hidemyself']],
-               array( 'hidemyself' => 1-$options['hidemyself'] ), $nondefaults);
-
-       $links[] = wfMsgHtml( 'rcshowhideminor', $minorLink );
-       $links[] = wfMsgHtml( 'rcshowhidebots', $botLink );
-       $links[] = wfMsgHtml( 'rcshowhideanons', $anonsLink );
-       $links[] = wfMsgHtml( 'rcshowhideliu', $liuLink );
-       if( $wgUser->useRCPatrol() )
-               $links[] = wfMsgHtml( 'rcshowhidepatr', $patrLink );
-       $links[] = wfMsgHtml( 'rcshowhidemine', $myselfLink );
-       $hl = implode( ' | ', $links );
-
-       // show from this onward link
-       $now = $wgLang->timeanddate( wfTimestampNow(), true );
-       $tl =  makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults );
-
-       $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'),
-               $cl, $dl, $hl );
-       $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl );
-       return "$note<br />$rclinks<br />$rclistfrom";
-
-}
-
-/**
- * Creates the choose namespace selection
- *
- * @private
- *
- * @param $namespace Mixed: the key of the currently selected namespace, empty string
- *              if there is none
- * @param $invert Bool: whether to invert the namespace selection
- * @param $nondefaults Array: an array of non default options to be remembered
- * @param $categories_any Bool: Default value for the checkbox
- *
- * @return string
- */
-function rcNamespaceForm( $namespace, $invert, $nondefaults, $categories_any ) {
-       global $wgScript, $wgAllowCategorizedRecentChanges, $wgRequest;
-       $t = SpecialPage::getTitleFor( 'Recentchanges' );
-
-       $namespaceselect = HTMLnamespaceselector($namespace, '');
-       $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . "\" />\n";
-       $invertbox = "<input type='checkbox' name='invert' value='1' id='nsinvert'" . ( $invert ? ' checked="checked"' : '' ) . ' />';
-
-       if ( $wgAllowCategorizedRecentChanges ) {
-               $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
-               $cb_arr = array( 'type' => 'checkbox', 'name' => 'categories_any', 'value' => "1" ) ;
-               if ( $categories_any ) $cb_arr['checked'] = "checked" ;
-               $catbox = "<br />" ;
-               $catbox .= wfMsgExt('rc_categories', array('parseinline')) . " ";
-               $catbox .= wfElement('input', array( 'type' => 'text', 'name' => 'categories', 'value' => $categories));
-               $catbox .= " &nbsp;" ;
-               $catbox .= wfElement('input', $cb_arr );
-               $catbox .= wfMsgExt('rc_categories_any', array('parseinline'));
-       } else {
-               $catbox = "" ;
-       }
-
-       $out = "<div class='namespacesettings'><form method='get' action='{$wgScript}'>\n";
-
-       foreach ( $nondefaults as $key => $value ) {
-               if ($key != 'namespace' && $key != 'invert')
-                       $out .= wfElement('input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value));
-       }
-
-       $out .= '<input type="hidden" name="title" value="'.$t->getPrefixedText().'" />';
-       $out .= "
-<div id='nsselect' class='recentchanges'>
-       <label for='namespace'>" . wfMsgHtml('namespace') . "</label>
-       {$namespaceselect}{$submitbutton}{$invertbox} <label for='nsinvert'>" . wfMsgHtml('invert') . "</label>{$catbox}\n</div>";
-       $out .= '</form></div>';
-       return $out;
-}
-
-
-/**
- * Format a diff for the newsfeed
- */
-function rcFormatDiff( $row ) {
-       global $wgUser;
-
-       $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
-       $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp );
-       $actiontext = '';
-       if( $row->rc_type == RC_LOG ) {
-               if( $row->rc_deleted & LogPage::DELETED_ACTION ) {
-                       $actiontext = wfMsgHtml('rev-deleted-event');
-               } else {
-                       $actiontext = LogPage::actionText( $row->rc_log_type, $row->rc_log_action,
-                               $titleObj, $wgUser->getSkin(), LogPage::extractParams($row->rc_params,true,true) );
-               }
-       }
-       return rcFormatDiffRow( $titleObj,
-               $row->rc_last_oldid, $row->rc_this_oldid,
-               $timestamp,
-               ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMsgHtml('rev-deleted-comment') : $row->rc_comment,
-               $actiontext );
-}
-
-function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
-       global $wgFeedDiffCutoff, $wgContLang, $wgUser;
-       wfProfileIn( __FUNCTION__ );
-
-       $skin = $wgUser->getSkin();
-       # log enties
-       if( $actiontext ) {
-               $comment = "$actiontext $comment";
-       }
-       $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n";
-
-       //NOTE: Check permissions for anonymous users, not current user.
-       //      No "privileged" version should end up in the cache.
-       //      Most feed readers will not log in anway.
-       $anon = new User();
-       $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true );
-
-       if( $title->getNamespace() >= 0 && !$accErrors ) {
-               if( $oldid ) {
-                       wfProfileIn( __FUNCTION__."-dodiff" );
-
-                       $de = new DifferenceEngine( $title, $oldid, $newid );
-                       #$diffText = $de->getDiff( wfMsg( 'revisionasof',
-                       #       $wgContLang->timeanddate( $timestamp ) ),
-                       #       wfMsg( 'currentrev' ) );
-                       $diffText = $de->getDiff(
-                               wfMsg( 'previousrevision' ), // hack
-                               wfMsg( 'revisionasof',
-                                       $wgContLang->timeanddate( $timestamp ) ) );
-
-
-                       if ( strlen( $diffText ) > $wgFeedDiffCutoff ) {
-                               // Omit large diffs
-                               $diffLink = $title->escapeFullUrl(
-                                       'diff=' . $newid .
-                                       '&oldid=' . $oldid );
-                               $diffText = '<a href="' .
-                                       $diffLink .
-                                       '">' .
-                                       htmlspecialchars( wfMsgForContent( 'difference' ) ) .
-                                       '</a>';
-                       } elseif ( $diffText === false ) {
-                               // Error in diff engine, probably a missing revision
-                               $diffText = "<p>Can't load revision $newid</p>";
-                       } else {
-                               // Diff output fine, clean up any illegal UTF-8
-                               $diffText = UtfNormal::cleanUp( $diffText );
-                               $diffText = rcApplyDiffStyle( $diffText );
-                       }
-                       wfProfileOut( __FUNCTION__."-dodiff" );
-               } else {
-                       $rev = Revision::newFromId( $newid );
-                       if( is_null( $rev ) ) {
-                               $newtext = '';
-                       } else {
-                               $newtext = $rev->getText();
-                       }
-                       $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' .
-                               '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
-               }
-               $completeText .= $diffText;
-       }
-
-       wfProfileOut( __FUNCTION__ );
-       return $completeText;
-}
-
-/**
- * Hacky application of diff styles for the feeds.
- * Might be 'cleaner' to use DOM or XSLT or something,
- * but *gack* it's a pain in the ass.
- *
- * @param $text String:
- * @return string
- * @private
- */
-function rcApplyDiffStyle( $text ) {
-       $styles = array(
-               'diff'             => 'background-color: white; color:black;',
-               'diff-otitle'      => 'background-color: white; color:black;',
-               'diff-ntitle'      => 'background-color: white; color:black;',
-               'diff-addedline'   => 'background: #cfc; color:black; font-size: smaller;',
-               'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;',
-               'diff-context'     => 'background: #eee; color:black; font-size: smaller;',
-               'diffchange'       => 'color: red; font-weight: bold; text-decoration: none;',
-       );
-
-       foreach( $styles as $class => $style ) {
-               $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/",
-                       "\\1style=\"$style\"\\3", $text );
-       }
-
-       return $text;
-}
diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php
deleted file mode 100644 (file)
index 12a1a23..0000000
+++ /dev/null
@@ -1,192 +0,0 @@
-<?php
-/**
- * This is to display changes made to all articles linked in an article.
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-require_once( 'SpecialRecentchanges.php' );
-
-/**
- * Entrypoint
- * @param string $par parent page we will look at
- */
-function wfSpecialRecentchangeslinked( $par = NULL ) {
-       global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgTitle, $wgScript;
-
-       $days = $wgRequest->getInt( 'days' );
-       $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
-       $hideminor = $wgRequest->getBool( 'hideminor' ) ? 1 : 0;
-       $showlinkedto = $wgRequest->getBool( 'showlinkedto' ) ? 1 : 0;
-
-       $title = Title::newFromURL( $target );
-       $target = $title ? $title->getPrefixedText() : '';
-
-       $wgOut->setPagetitle( wfMsg( 'recentchangeslinked' ) );
-       $sk = $wgUser->getSkin();
-
-       $wgOut->addHTML(
-               Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
-               Xml::openElement( 'fieldset' ) .
-               Xml::element( 'legend', array(), wfMsg( 'recentchangeslinked' ) ) . "\n" .
-               Xml::inputLabel( wfMsg( 'recentchangeslinked-page' ), 'target', 'recentchangeslinked-target', 40, $target ) .
-               "&nbsp;&nbsp;&nbsp;<span style='white-space: nowrap'>" .
-               Xml::check( 'showlinkedto', $showlinkedto, array('id' => 'showlinkedto') ) . ' ' .
-               Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) .
-               "</span><br/>\n" .
-               Xml::hidden( 'title', $wgTitle->getPrefixedText() ). "\n" .
-               Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
-               Xml::closeElement( 'fieldset' ) .
-               Xml::closeElement( 'form' ) . "\n"
-       );
-
-       if ( !$target ) {
-               return;
-       }
-       $nt = Title::newFromURL( $target );
-       if( !$nt ) {
-               $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
-               return;
-       }
-       $id = $nt->getArticleId();
-
-       $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) );
-       $wgOut->setSyndicated();
-       $wgOut->setFeedAppendQuery( "target=" . urlencode( $target ) );
-
-       if ( !$days ) {
-               $days = (int)$wgUser->getOption( 'rcdays', 7 );
-       }
-       list( $limit, /* offset */ ) = wfCheckLimits( 100, 'rclimit' );
-
-       $dbr = wfGetDB( DB_SLAVE,'recentchangeslinked' );
-       $cutoff = $dbr->timestamp( time() - ( $days * 86400 ) );
-
-       $hideminor = ($hideminor ? 1 : 0);
-       if ( $hideminor ) {
-               $mlink = $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchangeslinked' ),
-                 wfMsg( 'show' ), 'target=' . htmlspecialchars( $nt->getPrefixedURL() ) .
-                 "&days={$days}&limit={$limit}&hideminor=0&showlinkedto={$showlinkedto}" );
-       } else {
-               $mlink = $sk->makeKnownLink( $wgContLang->specialPage( "Recentchangeslinked" ),
-                 wfMsg( "hide" ), "target=" . htmlspecialchars( $nt->getPrefixedURL() ) .
-                 "&days={$days}&limit={$limit}&hideminor=1&showlinkedto={$showlinkedto}" );
-       }
-       if ( $hideminor ) {
-               $cmq = 'AND rc_minor=0';
-       } else { $cmq = ''; }
-
-       list($recentchanges, $categorylinks, $pagelinks, $watchlist) =
-           $dbr->tableNamesN( 'recentchanges', 'categorylinks', 'pagelinks', "watchlist" );
-
-       $uid = $wgUser->getId();
-       // The fields we are selecting
-       $fields = "rc_cur_id,rc_namespace,rc_title,
-               rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor,rc_log_type,rc_log_action,rc_params,rc_deleted,
-               rc_new, rc_id, rc_this_oldid, rc_last_oldid, rc_bot, rc_patrolled, rc_type, rc_old_len, rc_new_len";
-       $fields .= $uid ? ",wl_user" : "";
-
-       // Check if this should be a feed
-       $feed = false;
-       global $wgSitename, $wgFeedClasses, $wgContLanguageCode, $wgFeedLimit;
-       $feedFormat = $wgRequest->getVal( 'feed' );
-       if( $feedFormat && isset( $wgFeedClasses[$feedFormat] ) ) {
-               $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchangeslinked-title', $nt->getPrefixedText() ) . 
-                       ' [' . $wgContLanguageCode . ']';
-               $feed = new $wgFeedClasses[$feedFormat]( $feedTitle, 
-                       htmlspecialchars( wfMsgForContent('recentchangeslinked') ), $wgTitle->getFullUrl() );
-               # Sanity check
-               if( $limit > $wgFeedLimit ) {
-                       $limit = $wgFeedLimit;
-               }
-       }
-
-       // If target is a Category, use categorylinks and invert from and to
-       if( $nt->getNamespace() == NS_CATEGORY ) {
-               $catkey = $dbr->addQuotes( $nt->getDBkey() );
-               # The table clauses
-               $tables = "$categorylinks, $recentchanges";
-               $tables .= $uid ? " LEFT JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "";
-
-               $sql = "SELECT /* wfSpecialRecentchangeslinked */ $fields FROM $tables 
-                       WHERE rc_timestamp > '{$cutoff}' {$cmq} 
-                       AND cl_from=rc_cur_id 
-                       AND cl_to=$catkey 
-                       GROUP BY $fields ORDER BY rc_timestamp DESC LIMIT {$limit}"; // Shitty-ass GROUP BY by for postgres apparently
-       } else {
-               if( $showlinkedto ) {
-                       $ns = $dbr->addQuotes( $nt->getNamespace() );
-                       $dbkey = $dbr->addQuotes( $nt->getDBkey() );
-                       $joinConds = "AND pl_namespace={$ns} AND pl_title={$dbkey} AND pl_from=rc_cur_id";
-               } else {
-                       $joinConds = "AND pl_namespace=rc_namespace AND pl_title=rc_title AND pl_from=$id";
-               }
-               # The table clauses
-               $tables = "$pagelinks, $recentchanges";
-               $tables .= $uid ? " LEFT JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "";
-
-               $sql = "SELECT /* wfSpecialRecentchangeslinked */ $fields FROM $tables
-                       WHERE rc_timestamp > '{$cutoff}' {$cmq}
-                       {$joinConds}
-                       GROUP BY $fields ORDER BY rc_timestamp DESC LIMIT {$limit}"; // Shitty-ass GROUP BY by for postgres apparently
-       }
-       # Actually do the query
-       $res = $dbr->query( $sql, __METHOD__ );
-       $count = $dbr->numRows( $res );
-       $rchanges = array();
-       # Output feeds now and be done with it!
-       if( $feed ) {
-               if( $count ) {
-                       $counter = 1;
-                       while ( $limit ) {
-                               if ( 0 == $count ) { break; }
-                               $obj = $dbr->fetchObject( $res );
-                               --$count;
-                               $rc = RecentChange::newFromRow( $obj );
-                               $rc->counter = $counter++;
-                               --$limit;
-                               $rchanges[] = $obj;
-                       }
-               }
-               require_once( "SpecialRecentchanges.php" );
-               $wgOut->disable();
-               rcDoOutputFeed( $rchanges, $feed );
-               return;
-       }
-       
-       # Otherwise, carry on with regular output...
-       $wgOut->addHTML("&lt; ".$sk->makeLinkObj($nt, "", "redirect=no" )."<br />\n");
-       $note = wfMsgExt( "rcnote", array ( 'parseinline' ), $limit, $days, $wgLang->timeAndDate( wfTimestampNow(), true ) );
-       $wgOut->addHTML( "<hr />\n{$note}\n<br />" );
-
-       $note = rcDayLimitlinks( $days, $limit, "Recentchangeslinked",
-                                "target=" . $nt->getPrefixedURL() . "&hideminor={$hideminor}&showlinkedto={$showlinkedto}",
-                                false, $mlink );
-
-       $wgOut->addHTML( $note."\n" );
-
-       $list = ChangesList::newFromUser( $wgUser );
-       $s = $list->beginRecentChangesList();
-
-       if ( $count ) {
-               $counter = 1;
-               while ( $limit ) {
-                       if ( 0 == $count ) { break; }
-                       $obj = $dbr->fetchObject( $res );
-                       --$count;
-                       $rc = RecentChange::newFromRow( $obj );
-                       $rc->counter = $counter++;
-                       $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) );
-                       --$limit;
-               }
-       } else {
-               $wgOut->addWikiMsg('recentchangeslinked-noresult');
-       }
-       $s .= $list->endRecentChangesList();
-
-       $dbr->freeResult( $res );
-       $wgOut->addHTML( $s );
-}
diff --git a/includes/SpecialResetpass.php b/includes/SpecialResetpass.php
deleted file mode 100644 (file)
index 707b941..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/** Constructor */
-function wfSpecialResetpass( $par ) {
-       $form = new PasswordResetForm();
-       $form->execute( $par );
-}
-
-/**
- * Let users recover their password.
- * @ingroup SpecialPage
- */
-class PasswordResetForm extends SpecialPage {
-       function __construct( $name=null, $reset=null ) {
-               if( $name !== null ) {
-                       $this->mName = $name;
-                       $this->mTemporaryPassword = $reset;
-               } else {
-                       global $wgRequest;
-                       $this->mName = $wgRequest->getVal( 'wpName' );
-                       $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' );
-               }
-       }
-
-       /**
-        * Main execution point
-        */
-       function execute( $par ) {
-               global $wgUser, $wgAuth, $wgOut, $wgRequest;
-
-               if( !$wgAuth->allowPasswordChange() ) {
-                       $this->error( wfMsg( 'resetpass_forbidden' ) );
-                       return;
-               }
-
-               if( $this->mName === null && !$wgRequest->wasPosted() ) {
-                       $this->error( wfMsg( 'resetpass_missing' ) );
-                       return;
-               }
-
-               if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
-                       $newpass = $wgRequest->getVal( 'wpNewPassword' );
-                       $retype = $wgRequest->getVal( 'wpRetype' );
-                       try {
-                               $this->attemptReset( $newpass, $retype );
-                               $wgOut->addWikiMsg( 'resetpass_success' );
-
-                               $data = array(
-                                       'action' => 'submitlogin',
-                                       'wpName' => $this->mName,
-                                       'wpPassword' => $newpass,
-                                       'returnto' => $wgRequest->getVal( 'returnto' ),
-                               );
-                               if( $wgRequest->getCheck( 'wpRemember' ) ) {
-                                       $data['wpRemember'] = 1;
-                               }
-                               $login = new LoginForm( new FauxRequest( $data, true ) );
-                               $login->execute();
-
-                               return;
-                       } catch( PasswordError $e ) {
-                               $this->error( $e->getMessage() );
-                       }
-               }
-               $this->showForm();
-       }
-
-       function error( $msg ) {
-               global $wgOut;
-               $wgOut->addHtml( '<div class="errorbox">' .
-                       htmlspecialchars( $msg ) .
-                       '</div>' );
-       }
-
-       function showForm() {
-               global $wgOut, $wgUser, $wgRequest;
-
-               $wgOut->disallowUserJs();
-
-               $self = SpecialPage::getTitleFor( 'Resetpass' );
-               $form  =
-                       '<div id="userloginForm">' .
-                       wfOpenElement( 'form',
-                               array(
-                                       'method' => 'post',
-                                       'action' => $self->getLocalUrl() ) ) .
-                       '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' .
-                       '<div id="userloginprompt">' .
-                       wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
-                       '</div>' .
-                       '<table>' .
-                       wfHidden( 'token', $wgUser->editToken() ) .
-                       wfHidden( 'wpName', $this->mName ) .
-                       wfHidden( 'wpPassword', $this->mTemporaryPassword ) .
-                       wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
-                       $this->pretty( array(
-                               array( 'wpName', 'username', 'text', $this->mName ),
-                               array( 'wpNewPassword', 'newpassword', 'password', '' ),
-                               array( 'wpRetype', 'yourpasswordagain', 'password', '' ),
-                       ) ) .
-                       '<tr>' .
-                               '<td></td>' .
-                               '<td>' .
-                                       Xml::checkLabel( wfMsg( 'remembermypassword' ),
-                                               'wpRemember', 'wpRemember',
-                                               $wgRequest->getCheck( 'wpRemember' ) ) .
-                               '</td>' .
-                       '</tr>' .
-                       '<tr>' .
-                               '<td></td>' .
-                               '<td>' .
-                                       wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) .
-                               '</td>' .
-                       '</tr>' .
-                       '</table>' .
-                       wfCloseElement( 'form' ) .
-                       '</div>';
-               $wgOut->addHtml( $form );
-       }
-
-       function pretty( $fields ) {
-               $out = '';
-               foreach( $fields as $list ) {
-                       list( $name, $label, $type, $value ) = $list;
-                       if( $type == 'text' ) {
-                               $field = '<tt>' . htmlspecialchars( $value ) . '</tt>';
-                       } else {
-                               $field = Xml::input( $name, 20, $value,
-                                       array( 'id' => $name, 'type' => $type ) );
-                       }
-                       $out .= '<tr>';
-                       $out .= '<td align="right">';
-                       $out .= Xml::label( wfMsg( $label ), $name );
-                       $out .= '</td>';
-                       $out .= '<td>';
-                       $out .= $field;
-                       $out .= '</td>';
-                       $out .= '</tr>';
-               }
-               return $out;
-       }
-
-       /**
-        * @throws PasswordError when cannot set the new password because requirements not met.
-        */
-       function attemptReset( $newpass, $retype ) {
-               $user = User::newFromName( $this->mName );
-               if( $user->isAnon() ) {
-                       throw new PasswordError( 'no such user' );
-               }
-
-               if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) {
-                       throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) );
-               }
-
-               if( $newpass !== $retype ) {
-                       throw new PasswordError( wfMsg( 'badretype' ) );
-               }
-
-               $user->setPassword( $newpass );
-               $user->saveSettings();
-       }
-}
diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php
deleted file mode 100644 (file)
index a3d4b7e..0000000
+++ /dev/null
@@ -1,1470 +0,0 @@
-<?php
-/**
- * Special page allowing users with the appropriate permissions to view
- * and hide revisions. Log items can also be hidden.
- *
- * @file
- * @ingroup SpecialPage
- */
-
-function wfSpecialRevisiondelete( $par = null ) {
-       global $wgOut, $wgRequest, $wgUser;
-       # Handle our many different possible input types
-       $target = $wgRequest->getText( 'target' );
-       $oldid = $wgRequest->getArray( 'oldid' );
-       $artimestamp = $wgRequest->getArray( 'artimestamp' );
-       $logid = $wgRequest->getArray( 'logid' );
-       $img = $wgRequest->getArray( 'oldimage' );
-       $fileid = $wgRequest->getArray( 'fileid' );
-       # For reviewing deleted files...
-       $file = $wgRequest->getVal( 'file' );
-       # If this is a revision, then we need a target page
-       $page = Title::newFromUrl( $target );
-       if( is_null($page) ) {
-               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
-               return;
-       }
-       # Only one target set at a time please!
-       $i = (bool)$file + (bool)$oldid + (bool)$logid + (bool)$artimestamp + (bool)$fileid + (bool)$img;
-       if( $i !== 1 ) {
-               $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
-               return;
-       }
-       # Logs must have a type given
-       if( $logid && !strpos($page->getDBKey(),'/') ) {
-               $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
-               return;
-       }
-       # Either submit or create our form
-       $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $img, $file );
-       if( $wgRequest->wasPosted() ) {
-               $form->submit( $wgRequest );
-       } else if( $oldid || $artimestamp ) {
-               $form->showRevs();
-       } else if( $fileid || $img ) {
-               $form->showImages();
-       } else if( $logid ) {
-               $form->showLogItems();
-       }
-       # Show relevant lines from the deletion log. This will show even if said ID
-       # does not exist...might be helpful
-       $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
-       LogEventsList::showLogExtract( $wgOut, 'delete', $page->getPrefixedText() );
-       if( $wgUser->isAllowed( 'suppressionlog' ) ){
-               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
-               LogEventsList::showLogExtract( $wgOut, 'suppress', $page->getPrefixedText() );
-       }
-}
-
-/**
- * Implements the GUI for Revision Deletion.
- * @ingroup SpecialPage
- */
-class RevisionDeleteForm {
-       /**
-        * @param Title $page
-        * @param array $oldids
-        * @param array $logids
-        * @param array $artimestamps
-        * @param array $fileids
-        * @param array $img
-        * @param string $file
-        */
-       function __construct( $page, $oldids, $logids, $artimestamps, $fileids, $img, $file ) {
-               global $wgUser, $wgOut;
-
-               $this->page = $page;
-               # For reviewing deleted files...
-               if( $file ) {
-                       $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file );
-                       $oimage->load();
-                       // Check if user is allowed to see this file
-                       if( !$oimage->userCan(File::DELETED_FILE) ) {
-                               $wgOut->permissionRequired( 'suppressrevision' );
-                       } else {
-                               $this->showFile( $file );
-                       }
-                       return;
-               }
-               $this->skin = $wgUser->getSkin();
-               # Give a link to the log for this page
-               if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
-                       $links = array();
-
-                       $logtitle = SpecialPage::getTitleFor( 'Log' );
-                       $links[] = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
-                               wfArrayToCGI( array( 'page' => $this->page->getPrefixedUrl() ) ) );
-                       # Give a link to the page history
-                       $links[] = $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml( 'pagehist' ),
-                               wfArrayToCGI( array( 'action' => 'history' ) ) );
-                       # Link to deleted edits
-                       if( $wgUser->isAllowed('undelete') ) {
-                               $undelete = SpecialPage::getTitleFor( 'Undelete' );
-                               $links[] = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'deletedhist' ),
-                                       wfArrayToCGI( array( 'target' => $this->page->getPrefixedUrl() ) ) );
-                       }
-                       # Logs themselves don't have histories or archived revisions
-                       $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
-               }
-               // At this point, we should only have one of these
-               if( $oldids ) {
-                       $this->revisions = $oldids;
-                       $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
-                       $this->deleteKey='oldid';
-               } else if( $artimestamps ) {
-                       $this->archrevs = $artimestamps;
-                       $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
-                       $this->deleteKey='artimestamp';
-               } else if( $img ) {
-                       $this->ofiles = $img;
-                       $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
-                       $this->deleteKey='oldimage';
-               } else if( $fileids ) {
-                       $this->afiles = $fileids;
-                       $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
-                       $this->deleteKey='fileid';
-               } else if( $logids ) {
-                       $this->events = $logids;
-                       $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogPage::DELETED_ACTION );
-                       $this->deleteKey='logid';
-               }
-               // Our checkbox messages depends one what we are doing,
-               // e.g. we don't hide "text" for logs or images
-               $this->checks = array(
-                       $hide_content_name,
-                       array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
-                       array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ) );
-               if( $wgUser->isAllowed('suppressrevision') ) {
-                       $this->checks[] = array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED );
-               }
-       }
-
-       /**
-        * Show a deleted file version requested by the visitor.
-        */
-       private function showFile( $key ) {
-               global $wgOut, $wgRequest;
-               $wgOut->disable();
-
-               # We mustn't allow the output to be Squid cached, otherwise
-               # if an admin previews a deleted image, and it's cached, then
-               # a user without appropriate permissions can toddle off and
-               # nab the image, and Squid will serve it
-               $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
-               $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
-               $wgRequest->response()->header( 'Pragma: no-cache' );
-
-               $store = FileStore::get( 'deleted' );
-               $store->stream( $key );
-       }
-
-       /**
-        * This lets a user set restrictions for live and archived revisions
-        */
-       function showRevs() {
-               global $wgOut, $wgUser, $action;
-
-               $UserAllowed = true;
-
-               $count = ($this->deleteKey=='oldid') ?
-                       count($this->revisions) : count($this->archrevs);
-               $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
-
-               $bitfields = 0;
-               $wgOut->addHtml( "<ul>" );
-
-               $where = $revObjs = array();
-               $dbr = wfGetDB( DB_SLAVE );
-               
-               $revisions = 0;
-               // Live revisions...
-               if( $this->deleteKey=='oldid' ) {
-                       // Run through and pull all our data in one query
-                       foreach( $this->revisions as $revid ) {
-                               $where[] = intval($revid);
-                       }
-                       $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
-                       $result = $dbr->select( array('revision','page'), '*',
-                               array( 'rev_page' => $this->page->getArticleID(),
-                                       $whereClause, 'rev_page = page_id' ),
-                               __METHOD__ );
-                       while( $row = $dbr->fetchObject( $result ) ) {
-                               $revObjs[$row->rev_id] = new Revision( $row );
-                       }
-                       foreach( $this->revisions as $revid ) {
-                               // Hiding top revisison is bad
-                               if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
-                                       continue;
-                               } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
-                               // If a rev is hidden from sysops
-                                       if( $action != 'submit') {
-                                               $wgOut->permissionRequired( 'suppressrevision' );
-                                               return;
-                                       }
-                                       $UserAllowed = false;
-                               }
-                               $revisions++;
-                               $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) );
-                               $bitfields |= $revObjs[$revid]->mDeleted;
-                       }
-               // The archives...
-               } else {
-                       // Run through and pull all our data in one query
-                       foreach( $this->archrevs as $timestamp ) {
-                               $where[] = $dbr->addQuotes( $timestamp );
-                       }
-                       $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
-                       $result = $dbr->select( 'archive', '*',
-                               array( 'ar_namespace' => $this->page->getNamespace(),
-                                       'ar_title' => $this->page->getDBKey(),
-                                               $whereClause ),
-                               __METHOD__ );
-                       while( $row = $dbr->fetchObject( $result ) ) {
-                               $revObjs[$row->ar_timestamp] = new Revision( array(
-                               'page'       => $this->page->getArticleId(),
-                               'id'         => $row->ar_rev_id,
-                               'text'       => $row->ar_text_id,
-                               'comment'    => $row->ar_comment,
-                               'user'       => $row->ar_user,
-                               'user_text'  => $row->ar_user_text,
-                               'timestamp'  => $row->ar_timestamp,
-                               'minor_edit' => $row->ar_minor_edit,
-                               'text_id'    => $row->ar_text_id,
-                               'deleted'    => $row->ar_deleted,
-                               'len'        => $row->ar_len) );
-                       }
-                       foreach( $this->archrevs as $timestamp ) {
-                               if( !isset($revObjs[$timestamp]) ) {
-                                       continue;
-                               } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
-                               // If a rev is hidden from sysops
-                                       if( $action != 'submit') {
-                                               $wgOut->permissionRequired( 'suppressrevision' );
-                                               return;
-                                       }
-                                       $UserAllowed = false;
-                               }
-                               $revisions++;
-                               $wgOut->addHtml( $this->historyLine( $revObjs[$timestamp] ) );
-                               $bitfields |= $revObjs[$timestamp]->mDeleted;
-                       }
-               }
-               if( !$revisions ) {
-                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
-                       return;
-               }
-               
-               $wgOut->addHtml( "</ul>" );
-
-               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
-
-               // Normal sysops can always see what they did, but can't always change it
-               if( !$UserAllowed ) return;
-
-               $items = array(
-                       Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
-                       Xml::submitButton( wfMsg( 'revdelete-submit' ) )
-               );
-               $hidden = array(
-                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
-                       Xml::hidden( 'target', $this->page->getPrefixedText() ),
-                       Xml::hidden( 'type', $this->deleteKey )
-               );
-               if( $this->deleteKey=='oldid' ) {
-                       foreach( $revObjs as $rev )
-                               $hidden[] = wfHidden( 'oldid[]', $rev->getId() );
-               } else {
-                       foreach( $revObjs as $rev )
-                               $hidden[] = wfHidden( 'artimestamp[]', $rev->getTimestamp() );
-               }
-               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
-               $wgOut->addHtml(
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 
-                               'id' => 'mw-revdel-form-revisions' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       xml::element( 'legend', null,  wfMsg( 'revdelete-legend' ) )
-               );
-               // FIXME: all items checked for just one rev are checked, even if not set for the others
-               foreach( $this->checks as $item ) {
-                       list( $message, $name, $field ) = $item;
-                       $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
-               }
-               foreach( $items as $item ) {
-                       $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
-               }
-               foreach( $hidden as $item ) {
-                       $wgOut->addHtml( $item );
-               }
-               $wgOut->addHtml(
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-
-       }
-
-       /**
-        * This lets a user set restrictions for archived images
-        */
-       function showImages() {
-               global $wgOut, $wgUser, $action;
-
-               $UserAllowed = true;
-
-               $count = ($this->deleteKey=='oldimage') ? count($this->ofiles) : count($this->afiles);
-               $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'),
-                       $this->page->getPrefixedText(), $count ) );
-
-               $bitfields = 0;
-               $wgOut->addHtml( "<ul>" );
-
-               $where = $filesObjs = array();
-               $dbr = wfGetDB( DB_SLAVE );
-               // Live old revisions...
-               $revisions = 0;
-               if( $this->deleteKey=='oldimage' ) {
-                       // Run through and pull all our data in one query
-                       foreach( $this->ofiles as $timestamp ) {
-                               $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() );
-                       }
-                       $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
-                       $result = $dbr->select( 'oldimage', '*',
-                               array( 'oi_name' => $this->page->getDbKey(),
-                                       $whereClause ),
-                               __METHOD__ );
-                       while( $row = $dbr->fetchObject( $result ) ) {
-                               $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
-                               $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
-                               $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
-                       }
-                       // Check through our images
-                       foreach( $this->ofiles as $timestamp ) {
-                               $archivename = $timestamp.'!'.$this->page->getDbKey();
-                               if( !isset($filesObjs[$archivename]) ) {
-                                       continue;
-                               } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
-                                       // If a rev is hidden from sysops
-                                       if( $action != 'submit' ) {
-                                               $wgOut->permissionRequired( 'suppressrevision' );
-                                               return;
-                                       }
-                                       $UserAllowed = false;
-                               }
-                               $revisions++;
-                               // Inject history info
-                               $wgOut->addHtml( $this->fileLine( $filesObjs[$archivename] ) );
-                               $bitfields |= $filesObjs[$archivename]->deleted;
-                       }
-               // Archived files...
-               } else {
-                       // Run through and pull all our data in one query
-                       foreach( $this->afiles as $id ) {
-                               $where[] = intval($id);
-                       }
-                       $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
-                       $result = $dbr->select( 'filearchive', '*',
-                               array( 'fa_name' => $this->page->getDbKey(),
-                                       $whereClause ),
-                               __METHOD__ );
-                       while( $row = $dbr->fetchObject( $result ) ) {
-                               $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
-                       }
-
-                       foreach( $this->afiles as $fileid ) {
-                               if( !isset($filesObjs[$fileid]) ) {
-                                       continue;
-                               } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
-                                       // If a rev is hidden from sysops
-                                       if( $action != 'submit' ) {
-                                               $wgOut->permissionRequired( 'suppressrevision' );
-                                               return;
-                                       }
-                                       $UserAllowed = false;
-                               }
-                               $revisions++;
-                               // Inject history info
-                               $wgOut->addHtml( $this->archivedfileLine( $filesObjs[$fileid] ) );
-                               $bitfields |= $filesObjs[$fileid]->deleted;
-                       }
-               }
-               if( !$revisions ) {
-                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
-                       return;
-               }
-               
-               $wgOut->addHtml( "</ul>" );
-
-               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
-               //Normal sysops can always see what they did, but can't always change it
-               if( !$UserAllowed ) return;
-
-               $items = array(
-                       Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
-                       Xml::submitButton( wfMsg( 'revdelete-submit' ) )
-               );
-               $hidden = array(
-                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
-                       Xml::hidden( 'target', $this->page->getPrefixedText() ),
-                       Xml::hidden( 'type', $this->deleteKey )
-               );
-               if( $this->deleteKey=='oldimage' ) {
-                       foreach( $this->ofiles as $filename )
-                               $hidden[] = wfHidden( 'oldimage[]', $filename );
-               } else {
-                       foreach( $this->afiles as $fileid )
-                               $hidden[] = wfHidden( 'fileid[]', $fileid );
-               }
-               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
-               $wgOut->addHtml(
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 
-                               'id' => 'mw-revdel-form-filerevisions' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       xml::element( 'legend', null,  wfMsg( 'revdelete-legend' ) )
-               );
-               // FIXME: all items checked for just one file are checked, even if not set for the others
-               foreach( $this->checks as $item ) {
-                       list( $message, $name, $field ) = $item;
-                       $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
-               }
-               foreach( $items as $item ) {
-                       $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
-               }
-               foreach( $hidden as $item ) {
-                       $wgOut->addHtml( $item );
-               }
-
-               $wgOut->addHtml(
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-       }
-
-       /**
-        * This lets a user set restrictions for log items
-        */
-       function showLogItems() {
-               global $wgOut, $wgUser, $action, $wgMessageCache;
-
-               $UserAllowed = true;
-               $wgOut->addWikiText( wfMsgExt( 'logdelete-selected', array('parsemag'), count($this->events) ) );
-
-               $bitfields = 0;
-               $wgOut->addHtml( "<ul>" );
-
-               $where = $logRows = array();
-               $dbr = wfGetDB( DB_SLAVE );
-               // Run through and pull all our data in one query
-               $logItems = 0;
-               foreach( $this->events as $logid ) {
-                       $where[] = intval($logid);
-               }
-               list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
-               $whereClause = "log_type = '$logtype' AND log_id IN(" . implode(',',$where) . ")";
-               $result = $dbr->select( 'logging', '*',
-                       array( $whereClause ),
-                       __METHOD__ );
-               while( $row = $dbr->fetchObject( $result ) ) {
-                       $logRows[$row->log_id] = $row;
-               }
-               $wgMessageCache->loadAllMessages();
-               foreach( $this->events as $logid ) {
-                       // Don't hide from oversight log!!!
-                       if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='suppress' ) {
-                               continue;
-                       } else if( !LogEventsList::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
-                       // If an event is hidden from sysops
-                               if( $action != 'submit') {
-                                       $wgOut->permissionRequired( 'suppressrevision' );
-                                       return;
-                               }
-                               $UserAllowed = false;
-                       }
-                       $logItems++;
-                       $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
-                       $bitfields |= $logRows[$logid]->log_deleted;
-               }
-               if( !$logItems ) {
-                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
-                       return;
-               }
-               
-               $wgOut->addHtml( "</ul>" );
-
-               $wgOut->addWikiMsg( 'revdelete-text' );
-               // Normal sysops can always see what they did, but can't always change it
-               if( !$UserAllowed ) return;
-
-               $items = array(
-                       Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
-                       Xml::submitButton( wfMsg( 'revdelete-submit' ) ) );
-               $hidden = array(
-                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
-                       Xml::hidden( 'target', $this->page->getPrefixedText() ),
-                       Xml::hidden( 'type', $this->deleteKey ) );
-               foreach( $this->events as $logid ) {
-                       $hidden[] = Xml::hidden( 'logid[]', $logid );
-               }
-
-               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
-               $wgOut->addHtml(
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 
-                               'id' => 'mw-revdel-form-logs' ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       xml::element( 'legend', null,  wfMsg( 'revdelete-legend' ) )
-               );
-               // FIXME: all items checked for just on event are checked, even if not set for the others
-               foreach( $this->checks as $item ) {
-                       list( $message, $name, $field ) = $item;
-                       $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
-               }
-               foreach( $items as $item ) {
-                       $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
-               }
-               foreach( $hidden as $item ) {
-                       $wgOut->addHtml( $item );
-               }
-
-               $wgOut->addHtml(
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-       }
-
-       /**
-        * @param Revision $rev
-        * @returns string
-        */
-       private function historyLine( $rev ) {
-               global $wgContLang;
-
-               $date = $wgContLang->timeanddate( $rev->getTimestamp() );
-               $difflink = $del = '';
-               // Live revisions
-               if( $this->deleteKey=='oldid' ) {
-                       $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() );
-                       $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
-                               'diff=' . $rev->getId() . '&oldid=prev' ) . ')';
-               // Archived revisions
-               } else {
-                       $undelete = SpecialPage::getTitleFor( 'Undelete' );
-                       $target = $this->page->getPrefixedText();
-                       $revlink = $this->skin->makeLinkObj( $undelete, $date,
-                               "target=$target&timestamp=" . $rev->getTimestamp() );
-                       $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'),
-                               "target=$target&diff=prev&timestamp=" . $rev->getTimestamp() ) . ')';
-               }
-
-               if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $revlink = '<span class="history-deleted">'.$revlink.'</span>';
-                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
-                       if( !$rev->userCan(Revision::DELETED_TEXT) ) {
-                               $revlink = '<span class="history-deleted">'.$date.'</span>';
-                               $difflink = '(' . wfMsgHtml('diff') . ')';
-                       }
-               }
-
-               return "<li> $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del</li>";
-       }
-
-       /**
-        * @param File $file
-        * @returns string
-        */
-       private function fileLine( $file ) {
-               global $wgContLang, $wgTitle;
-
-               $target = $this->page->getPrefixedText();
-               $date = $wgContLang->timeanddate( $file->getTimestamp(), true  );
-
-               $del = '';
-               # Hidden files...
-               if( $file->isDeleted(File::DELETED_FILE) ) {
-                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
-                       if( !$file->userCan(File::DELETED_FILE) ) {
-                               $pageLink = $date;
-                       } else {
-                               $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
-                                       "target=$target&file=$file->sha1.".$file->getExtension() );
-                       }
-                       $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
-               # Regular files...
-               } else {
-                       $url = $file->getUrlRel();
-                       $pageLink = "<a href=\"{$url}\">{$date}</a>";
-               }
-
-               $data = wfMsgHtml( 'widthheight',
-                                       $wgContLang->formatNum( $file->getWidth() ),
-                                       $wgContLang->formatNum( $file->getHeight() ) ) .
-                       ' (' . wfMsgHtml( 'nbytes', $wgContLang->formatNum( $file->getSize() ) ) . ')';
-
-               return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
-       }
-
-       /**
-        * @param ArchivedFile $file
-        * @returns string
-        */
-       private function archivedfileLine( $file ) {
-               global $wgContLang, $wgTitle;
-
-               $target = $this->page->getPrefixedText();
-               $date = $wgContLang->timeanddate( $file->getTimestamp(), true  );
-
-               $undelete = SpecialPage::getTitleFor( 'Undelete' );
-               $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" );
-
-               $del = '';
-               if( $file->isDeleted(File::DELETED_FILE) ) {
-                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
-               }
-
-               $data = wfMsgHtml( 'widthheight',
-                                       $wgContLang->formatNum( $file->getWidth() ),
-                                       $wgContLang->formatNum( $file->getHeight() ) ) .
-                       ' (' . wfMsgHtml( 'nbytes', $wgContLang->formatNum( $file->getSize() ) ) . ')';
-
-               return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
-       }
-
-       /**
-        * @param Array $row row
-        * @returns string
-        */
-       private function logLine( $row ) {
-               global $wgContLang;
-
-               $date = $wgContLang->timeanddate( $row->log_timestamp );
-               $paramArray = LogPage::extractParams( $row->log_params );
-               $title = Title::makeTitle( $row->log_namespace, $row->log_title );
-
-               $logtitle = SpecialPage::getTitleFor( 'Log' );
-               $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ),
-                       wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) );
-               // Action text
-               if( !LogEventsList::userCan($row,LogPage::DELETED_ACTION) ) {
-                       $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
-               } else {
-                       $action = LogPage::actionText( $row->log_type, $row->log_action, $title,
-                               $this->skin, $paramArray, true, true );
-                       if( $row->log_deleted & LogPage::DELETED_ACTION )
-                               $action = '<span class="history-deleted">' . $action . '</span>';
-               }
-               // User links
-               $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) );
-               if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) {
-                       $userLink = '<span class="history-deleted">' . $userLink . '</span>';
-               }
-               // Comment
-               $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
-               if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) {
-                       $comment = '<span class="history-deleted">' . $comment . '</span>';
-               }
-               return "<li>($loglink) $date $userLink $action $comment</li>";
-       }
-
-       /**
-        * Generate a user tool link cluster if the current user is allowed to view it
-        * @param ArchivedFile $file
-        * @return string HTML
-        */
-       private function fileUserTools( $file ) {
-               if( $file->userCan( Revision::DELETED_USER ) ) {
-                       $link = $this->skin->userLink( $file->user, $file->user_text ) .
-                               $this->skin->userToolLinks( $file->user, $file->user_text );
-               } else {
-                       $link = wfMsgHtml( 'rev-deleted-user' );
-               }
-               if( $file->isDeleted( Revision::DELETED_USER ) ) {
-                       return '<span class="history-deleted">' . $link . '</span>';
-               }
-               return $link;
-       }
-
-       /**
-        * Wrap and format the given file's comment block, if the current
-        * user is allowed to view it.
-        *
-        * @param ArchivedFile $file
-        * @return string HTML
-        */
-       private function fileComment( $file ) {
-               if( $file->userCan( File::DELETED_COMMENT ) ) {
-                       $block = $this->skin->commentBlock( $file->description );
-               } else {
-                       $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
-               }
-               if( $file->isDeleted( File::DELETED_COMMENT ) ) {
-                       return "<span class=\"history-deleted\">$block</span>";
-               }
-               return $block;
-       }
-
-       /**
-        * @param WebRequest $request
-        */
-       function submit( $request ) {
-               global $wgUser, $wgOut;
-
-               $bitfield = $this->extractBitfield( $request );
-               $comment = $request->getText( 'wpReason' );
-               # Can the user set this field?
-               if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) {
-                       $wgOut->permissionRequired( 'suppressrevision' );
-                       return false;
-               }
-               # If the save went through, go to success message. Otherwise
-               # bounce back to form...
-               if( $this->save( $bitfield, $comment, $this->page ) ) {
-                       $this->success();
-               } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
-                       return $this->showRevs();
-               } else if( $request->getCheck( 'logid' ) ) {
-                       return $this->showLogs();
-               } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
-                       return $this->showImages();
-               }
-       }
-
-       private function success() {
-               global $wgOut;
-
-               $wgOut->setPagetitle( wfMsgHtml( 'actioncomplete' ) );
-
-               if( $this->deleteKey=='logid' ) {
-                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'logdelete-success' ) ), false );
-                       $this->showLogItems();
-               } else if( $this->deleteKey=='oldid' || $this->deleteKey=='artimestamp' ) {
-                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false );
-                       $this->showRevs();
-               } else if( $this->deleteKey=='fileid' ) {
-                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false );
-                       $this->showImages();
-               } else if( $this->deleteKey=='oldimage' ) {
-                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false );
-                       $this->showImages();
-               }
-       }
-
-       /**
-        * Put together a rev_deleted bitfield from the submitted checkboxes
-        * @param WebRequest $request
-        * @return int
-        */
-       private function extractBitfield( $request ) {
-               $bitfield = 0;
-               foreach( $this->checks as $item ) {
-                       list( /* message */ , $name, $field ) = $item;
-                       if( $request->getCheck( $name ) ) {
-                               $bitfield |= $field;
-                       }
-               }
-               return $bitfield;
-       }
-
-       private function save( $bitfield, $reason, $title ) {
-               $dbw = wfGetDB( DB_MASTER );
-               // Don't allow simply locking the interface for no reason
-               if( $bitfield == Revision::DELETED_RESTRICTED ) {
-                       $bitfield = 0;
-               }
-               $deleter = new RevisionDeleter( $dbw );
-               // By this point, only one of the below should be set
-               if( isset($this->revisions) ) {
-                       return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
-               } else if( isset($this->archrevs) ) {
-                       return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
-               } else if( isset($this->ofiles) ) {
-                       return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
-               } else if( isset($this->afiles) ) {
-                       return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
-               } else if( isset($this->events) ) {
-                       return $deleter->setEventVisibility( $title, $this->events, $bitfield, $reason );
-               }
-       }
-}
-
-/**
- * Implements the actions for Revision Deletion.
- * @ingroup SpecialPage
- */
-class RevisionDeleter {
-       function __construct( $db ) {
-               $this->dbw = $db;
-       }
-
-       /**
-        * @param $title, the page these events apply to
-        * @param array $items list of revision ID numbers
-        * @param int $bitfield new rev_deleted value
-        * @param string $comment Comment for log records
-        */
-       function setRevVisibility( $title, $items, $bitfield, $comment ) {
-               global $wgOut;
-
-               $userAllowedAll = $success = true;
-               $revIDs = array();
-               $revCount = 0;
-               // Run through and pull all our data in one query
-               foreach( $items as $revid ) {
-                       $where[] = intval($revid);
-               }
-               $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
-               $result = $this->dbw->select( 'revision', '*',
-                       array( 'rev_page' => $title->getArticleID(),
-                               $whereClause ),
-                       __METHOD__ );
-               while( $row = $this->dbw->fetchObject( $result ) ) {
-                       $revObjs[$row->rev_id] = new Revision( $row );
-               }
-               // To work!
-               foreach( $items as $revid ) {
-                       if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
-                               $success = false;
-                               continue; // Must exist
-                       } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
-                       $userAllowedAll=false;
-                               continue;
-                       }
-                       // For logging, maintain a count of revisions
-                       if( $revObjs[$revid]->mDeleted != $bitfield ) {
-                               $revCount++;
-                               $revIDs[]=$revid;
-
-                               $this->updateRevision( $revObjs[$revid], $bitfield );
-                               $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
-                       }
-               }
-               // Clear caches...
-               // Don't log or touch if nothing changed
-               if( $revCount > 0 ) {
-                       $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted,
-                               $comment, $title, 'oldid', $revIDs );
-                       $this->updatePage( $title );
-               }
-               // Where all revs allowed to be set?
-               if( !$userAllowedAll ) {
-                       //FIXME: still might be confusing???
-                       $wgOut->permissionRequired( 'suppressrevision' );
-                       return false;
-               }
-
-               return $success;
-       }
-
-        /**
-        * @param $title, the page these events apply to
-        * @param array $items list of revision ID numbers
-        * @param int $bitfield new rev_deleted value
-        * @param string $comment Comment for log records
-        */
-       function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
-               global $wgOut;
-
-               $userAllowedAll = $success = true;
-               $count = 0;
-               $Id_set = array();
-               // Run through and pull all our data in one query
-               foreach( $items as $timestamp ) {
-                       $where[] = $this->dbw->addQuotes( $timestamp );
-               }
-               $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
-               $result = $this->dbw->select( 'archive', '*',
-                       array( 'ar_namespace' => $title->getNamespace(),
-                               'ar_title' => $title->getDBKey(),
-                                       $whereClause ),
-                       __METHOD__ );
-               while( $row = $this->dbw->fetchObject( $result ) ) {
-                       $revObjs[$row->ar_timestamp] = new Revision( array(
-                       'page'       => $title->getArticleId(),
-                       'id'         => $row->ar_rev_id,
-                       'text'       => $row->ar_text_id,
-                       'comment'    => $row->ar_comment,
-                       'user'       => $row->ar_user,
-                       'user_text'  => $row->ar_user_text,
-                       'timestamp'  => $row->ar_timestamp,
-                       'minor_edit' => $row->ar_minor_edit,
-                       'text_id'    => $row->ar_text_id,
-                       'deleted'    => $row->ar_deleted,
-                       'len'        => $row->ar_len) );
-               }
-               // To work!
-               foreach( $items as $timestamp ) {
-                       // This will only select the first revision with this timestamp.
-                       // Since they are all selected/deleted at once, we can just check the
-                       // permissions of one. UPDATE is done via timestamp, so all revs are set.
-                       if( !is_object($revObjs[$timestamp]) ) {
-                               $success = false;
-                               continue; // Must exist
-                       } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
-                       $userAllowedAll=false;
-                               continue;
-                       }
-                       // Which revisions did we change anything about?
-                       if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
-                          $Id_set[]=$timestamp;
-                          $count++;
-
-                          $this->updateArchive( $revObjs[$timestamp], $title, $bitfield );
-                       }
-               }
-               // For logging, maintain a count of revisions
-               if( $count > 0 ) {
-                       $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted,
-                               $comment, $title, 'artimestamp', $Id_set );
-               }
-               // Where all revs allowed to be set?
-               if( !$userAllowedAll ) {
-                       $wgOut->permissionRequired( 'suppressrevision' );
-                       return false;
-               }
-
-               return $success;
-       }
-
-        /**
-        * @param $title, the page these events apply to
-        * @param array $items list of revision ID numbers
-        * @param int $bitfield new rev_deleted value
-        * @param string $comment Comment for log records
-        */
-       function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
-               global $wgOut;
-
-               $userAllowedAll = $success = true;
-               $count = 0;
-               $set = array();
-               // Run through and pull all our data in one query
-               foreach( $items as $timestamp ) {
-                       $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
-               }
-               $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
-               $result = $this->dbw->select( 'oldimage', '*',
-                       array( 'oi_name' => $title->getDbKey(),
-                               $whereClause ),
-                       __METHOD__ );
-               while( $row = $this->dbw->fetchObject( $result ) ) {
-                       $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
-                       $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
-                       $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
-               }
-               // To work!
-               foreach( $items as $timestamp ) {
-                       $archivename = $timestamp.'!'.$title->getDbKey();
-                       if( !isset($filesObjs[$archivename]) ) {
-                               $success = false;
-                               continue; // Must exist
-                       } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
-                       $userAllowedAll=false;
-                               continue;
-                       }
-
-                       $transaction = true;
-                       // Which revisions did we change anything about?
-                       if( $filesObjs[$archivename]->deleted != $bitfield ) {
-                               $count++;
-
-                               $this->dbw->begin();
-                               $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
-                               // If this image is currently hidden...
-                               if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
-                                       if( $bitfield & File::DELETED_FILE ) {
-                                               # Leave it alone if we are not changing this...
-                                               $set[]=$archivename;
-                                               $transaction = true;
-                                       } else {
-                                               # We are moving this out
-                                               $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
-                                               $set[]=$transaction;
-                                       }
-                               // Is it just now becoming hidden?
-                               } else if( $bitfield & File::DELETED_FILE ) {
-                                       $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
-                                       $set[]=$transaction;
-                               } else {
-                                       $set[]=$timestamp;
-                               }
-                               // If our file operations fail, then revert back the db
-                               if( $transaction==false ) {
-                                       $this->dbw->rollback();
-                                       return false;
-                               }
-                               $this->dbw->commit();
-                       }
-               }
-
-               // Log if something was changed
-               if( $count > 0 ) {
-                       $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted,
-                               $comment, $title, 'oldimage', $set );
-                       # Purge page/history
-                       $file = wfLocalFile( $title );
-                       $file->purgeCache();
-                       $file->purgeHistory();
-                       # Invalidate cache for all pages using this file
-                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
-                       $update->doUpdate();
-               }
-               // Where all revs allowed to be set?
-               if( !$userAllowedAll ) {
-                       $wgOut->permissionRequired( 'suppressrevision' );
-                       return false;
-               }
-
-               return $success;
-       }
-
-        /**
-        * @param $title, the page these events apply to
-        * @param array $items list of revision ID numbers
-        * @param int $bitfield new rev_deleted value
-        * @param string $comment Comment for log records
-        */
-       function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
-               global $wgOut;
-
-               $userAllowedAll = $success = true;
-               $count = 0;
-               $Id_set = array();
-
-               // Run through and pull all our data in one query
-               foreach( $items as $id ) {
-                       $where[] = intval($id);
-               }
-               $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
-               $result = $this->dbw->select( 'filearchive', '*',
-                       array( 'fa_name' => $title->getDbKey(),
-                               $whereClause ),
-                       __METHOD__ );
-               while( $row = $this->dbw->fetchObject( $result ) ) {
-                       $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
-               }
-               // To work!
-               foreach( $items as $fileid ) {
-                       if( !isset($filesObjs[$fileid]) ) {
-                               $success = false;
-                               continue; // Must exist
-                       } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
-                       $userAllowedAll=false;
-                               continue;
-                       }
-                       // Which revisions did we change anything about?
-                       if( $filesObjs[$fileid]->deleted != $bitfield ) {
-                          $Id_set[]=$fileid;
-                          $count++;
-
-                          $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
-                       }
-               }
-               // Log if something was changed
-               if( $count > 0 ) {
-                       $this->updateLog( $title, $count, $bitfield, $comment,
-                               $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
-               }
-               // Where all revs allowed to be set?
-               if( !$userAllowedAll ) {
-                       $wgOut->permissionRequired( 'suppressrevision' );
-                       return false;
-               }
-
-               return $success;
-       }
-
-       /**
-        * @param $title, the log page these events apply to
-        * @param array $items list of log ID numbers
-        * @param int $bitfield new log_deleted value
-        * @param string $comment Comment for log records
-        */
-       function setEventVisibility( $title, $items, $bitfield, $comment ) {
-               global $wgOut;
-
-               $userAllowedAll = $success = true;
-               $count = 0;
-               $log_Ids = array();
-
-               // Run through and pull all our data in one query
-               foreach( $items as $logid ) {
-                       $where[] = intval($logid);
-               }
-               list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
-               $whereClause = "log_type ='$logtype' AND log_id IN(" . implode(',',$where) . ")";
-               $result = $this->dbw->select( 'logging', '*',
-                       array( $whereClause ),
-                       __METHOD__ );
-               while( $row = $this->dbw->fetchObject( $result ) ) {
-                       $logRows[$row->log_id] = $row;
-               }
-               // To work!
-               foreach( $items as $logid ) {
-                       if( !isset($logRows[$logid]) ) {
-                               $success = false;
-                               continue; // Must exist
-                       } else if( !LogEventsList::userCan($logRows[$logid], LogPage::DELETED_RESTRICTED)
-                                || $logRows[$logid]->log_type == 'suppress' ) {
-                       // Don't hide from oversight log!!!
-                       $userAllowedAll=false;
-                       continue;
-                       }
-                       // Which logs did we change anything about?
-                       if( $logRows[$logid]->log_deleted != $bitfield ) {
-                               $log_Ids[]=$logid;
-                               $count++;
-
-                               $this->updateLogs( $logRows[$logid], $bitfield );
-                               $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
-                       }
-               }
-               // Don't log or touch if nothing changed
-               if( $count > 0 ) {
-                       $this->updateLog( $title, $count, $bitfield, $logRows[$logid]->log_deleted,
-                               $comment, $title, 'logid', $log_Ids );
-               }
-               // Were all revs allowed to be set?
-               if( !$userAllowedAll ) {
-                       $wgOut->permissionRequired( 'suppressrevision' );
-                       return false;
-               }
-
-               return $success;
-       }
-
-       /**
-        * Moves an image to a safe private location
-        * Caller is responsible for clearing caches
-        * @param File $oimage
-        * @returns mixed, timestamp string on success, false on failure
-        */
-       function makeOldImagePrivate( $oimage ) {
-               $transaction = new FSTransaction();
-               if( !FileStore::lock() ) {
-                       wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
-                       return false;
-               }
-               $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
-               // Dupe the file into the file store
-               if( file_exists( $oldpath ) ) {
-                       // Is our directory configured?
-                       if( $store = FileStore::get( 'deleted' ) ) {
-                               if( !$oimage->sha1 ) {
-                                       $oimage->upgradeRow(); // sha1 may be missing
-                               }
-                               $key = $oimage->sha1 . '.' . $oimage->getExtension();
-                               $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
-                       } else {
-                               $group = null;
-                               $key = null;
-                               $transaction = false; // Return an error and do nothing
-                       }
-               } else {
-                       wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
-                       $group = null;
-                       $key = '';
-                       $transaction = new FSTransaction(); // empty
-               }
-
-               if( $transaction === false ) {
-                       // Fail to restore?
-                       wfDebug( __METHOD__.": import to file store failed, aborting\n" );
-                       throw new MWException( "Could not archive and delete file $oldpath" );
-                       return false;
-               }
-
-               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
-               $transaction->commit();
-               FileStore::unlock();
-
-               $m = explode('!',$oimage->archive_name,2);
-               $timestamp = $m[0];
-
-               return $timestamp;
-       }
-
-       /**
-        * Moves an image from a safe private location
-        * Caller is responsible for clearing caches
-        * @param File $oimage
-        * @returns mixed, string timestamp on success, false on failure
-        */
-       function makeOldImagePublic( $oimage ) {
-               $transaction = new FSTransaction();
-               if( !FileStore::lock() ) {
-                       wfDebug( __METHOD__." could not acquire filestore lock\n" );
-                       return false;
-               }
-
-               $store = FileStore::get( 'deleted' );
-               if( !$store ) {
-                       wfDebug( __METHOD__.": skipping row with no file.\n" );
-                       return false;
-               }
-
-               $key = $oimage->sha1.'.'.$oimage->getExtension();
-               $destDir = $oimage->getArchivePath();
-               if( !is_dir( $destDir ) ) {
-                       wfMkdirParents( $destDir );
-               }
-               $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
-               // Check if any other stored revisions use this file;
-               // if so, we shouldn't remove the file from the hidden
-               // archives so they will still work. Check hidden files first.
-               $useCount = $this->dbw->selectField( 'oldimage', '1',
-                       array( 'oi_sha1' => $oimage->sha1,
-                               'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
-                       __METHOD__, array( 'FOR UPDATE' ) );
-               // Check the rest of the deleted archives too.
-               // (these are the ones that don't show in the image history)
-               if( !$useCount ) {
-                       $useCount = $this->dbw->selectField( 'filearchive', '1',
-                               array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
-                               __METHOD__, array( 'FOR UPDATE' ) );
-               }
-
-               if( $useCount == 0 ) {
-                       wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
-                       $flags = FileStore::DELETE_ORIGINAL;
-               } else {
-                       $flags = 0;
-               }
-               $transaction->add( $store->export( $key, $destPath, $flags ) );
-
-               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
-               $transaction->commit();
-               FileStore::unlock();
-
-               $m = explode('!',$oimage->archive_name,2);
-               $timestamp = $m[0];
-
-               return $timestamp;
-       }
-
-       /**
-        * Update the revision's rev_deleted field
-        * @param Revision $rev
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateRevision( $rev, $bitfield ) {
-               $this->dbw->update( 'revision',
-                       array( 'rev_deleted' => $bitfield ),
-                       array( 'rev_id' => $rev->getId(),
-                               'rev_page' => $rev->getPage() ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Update the revision's rev_deleted field
-        * @param Revision $rev
-        * @param Title $title
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateArchive( $rev, $title, $bitfield ) {
-               $this->dbw->update( 'archive',
-                       array( 'ar_deleted' => $bitfield ),
-                       array( 'ar_namespace' => $title->getNamespace(),
-                               'ar_title'     => $title->getDBKey(),
-                               'ar_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ),
-                               'ar_rev_id' => $rev->getId() ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Update the images's oi_deleted field
-        * @param File $file
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateOldFiles( $file, $bitfield ) {
-               $this->dbw->update( 'oldimage',
-                       array( 'oi_deleted' => $bitfield ),
-                       array( 'oi_name' => $file->getName(),
-                               'oi_timestamp' => $this->dbw->timestamp( $file->getTimestamp() ) ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Update the images's fa_deleted field
-        * @param ArchivedFile $file
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateArchFiles( $file, $bitfield ) {
-               $this->dbw->update( 'filearchive',
-                       array( 'fa_deleted' => $bitfield ),
-                       array( 'fa_id' => $file->getId() ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Update the logging log_deleted field
-        * @param Row $row
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateLogs( $row, $bitfield ) {
-               $this->dbw->update( 'logging',
-                       array( 'log_deleted' => $bitfield ),
-                       array( 'log_id' => $row->log_id ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Update the revision's recentchanges record if fields have been hidden
-        * @param Revision $rev
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateRecentChangesEdits( $rev, $bitfield ) {
-               $this->dbw->update( 'recentchanges',
-                       array( 'rc_deleted' => $bitfield,
-                                  'rc_patrolled' => 1 ),
-                       array( 'rc_this_oldid' => $rev->getId(),
-                               'rc_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ) ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Update the revision's recentchanges record if fields have been hidden
-        * @param Row $row
-        * @param int $bitfield new rev_deleted bitfield value
-        */
-       function updateRecentChangesLog( $row, $bitfield ) {
-               $this->dbw->update( 'recentchanges',
-                       array( 'rc_deleted' => $bitfield,
-                                  'rc_patrolled' => 1 ),
-                       array( 'rc_logid' => $row->log_id,
-                               'rc_timestamp' => $row->log_timestamp ),
-                       __METHOD__ );
-       }
-
-       /**
-        * Touch the page's cache invalidation timestamp; this forces cached
-        * history views to refresh, so any newly hidden or shown fields will
-        * update properly.
-        * @param Title $title
-        */
-       function updatePage( $title ) {
-               $title->invalidateCache();
-               $title->purgeSquid();
-
-               // Extensions that require referencing previous revisions may need this
-               wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
-       }
-
-       /**
-        * Checks for a change in the bitfield for a certain option and updates the
-        * provided array accordingly.
-        *
-        * @param String $desc Description to add to the array if the option was
-        * enabled / disabled.
-        * @param int $field The bitmask describing the single option.
-        * @param int $diff The xor of the old and new bitfields.
-        * @param array $arr The array to update.
-        */
-       function checkItem ( $desc, $field, $diff, $new, &$arr ) {
-               if ( $diff & $field ) {
-                       $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc;
-               }
-       }
-
-       /**
-        * Gets an array describing the changes made to the visibilit of the revision.
-        * If the resulting array is $arr, then $arr[0] will contain an array of strings
-        * describing the items that were hidden, $arr[2] will contain an array of strings
-        * describing the items that were unhidden, and $arr[3] will contain an array with
-        * a single string, which can be one of "applied restrictions to sysops",
-        * "removed restrictions from sysops", or null.
-        *
-        * @param int $n The new bitfield.
-        * @param int $o The old bitfield.
-        * @return An array as described above.
-        */
-       function getChanges ( $n, $o ) {
-               $diff = $n ^ $o;
-               $ret = array ( 0 => array(), 1 => array(), 2 => array() );
-
-               $this->checkItem ( wfMsgForContent ( 'revdelete-content' ),
-                               Revision::DELETED_TEXT, $diff, $n, $ret );
-               $this->checkItem ( wfMsgForContent ( 'revdelete-summary' ),
-                               Revision::DELETED_COMMENT, $diff, $n, $ret );
-               $this->checkItem ( wfMsgForContent ( 'revdelete-uname' ),
-                               Revision::DELETED_USER, $diff, $n, $ret );
-
-               // Restriction application to sysops
-               if ( $diff & Revision::DELETED_RESTRICTED ) {
-                       if ( $n & Revision::DELETED_RESTRICTED )
-                               $ret[2][] = wfMsgForContent ( 'revdelete-restricted' );
-                       else
-                               $ret[2][] = wfMsgForContent ( 'revdelete-unrestricted' );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * Gets a log message to describe the given revision visibility change. This
-        * message will be of the form "[hid {content, edit summary, username}];
-        * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
-        *
-        * @param int $count The number of effected revisions.
-        * @param int $nbitfield The new bitfield for the revision.
-        * @param int $obitfield The old bitfield for the revision.
-        * @param string $comment The comment associated with the change.
-        * @param bool $isForLog
-        */
-       function getLogMessage ( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) {
-               global $wgContLang;
-
-               $s = '';
-               $changes = $this->getChanges( $nbitfield, $obitfield );
-
-               if ( count ( $changes[0] ) ) {
-                       $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) );
-               }
-
-               if ( count ( $changes[1] ) ) {
-                       if ($s) $s .= '; ';
-
-                       $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) );
-               }
-
-               if ( count ( $changes[2] )) {
-                       if ($s)
-                               $s .= ' (' . $changes[2][0] . ')';
-                       else
-                               $s = $changes[2][0];
-               }
-
-               $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
-               $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
-                       $s, $wgContLang->formatNum( $count ) );
-
-               if ( $comment )
-                       $ret .= ": $comment";
-
-               return $ret;
-
-       }
-
-       /**
-        * Record a log entry on the action
-        * @param Title $title, page where item was removed from
-        * @param int $count the number of revisions altered for this page
-        * @param int $nbitfield the new _deleted value
-        * @param int $obitfield the old _deleted value
-        * @param string $comment
-        * @param Title $target, the relevant page
-        * @param string $param, URL param
-        * @param Array $items
-        */
-       function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) {
-               // Put things hidden from sysops in the oversight log
-               $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'suppress' : 'delete';
-               $log = new LogPage( $logtype );
-
-               $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
-
-               if( $param == 'logid' ) {
-                       $params = array( implode( ',', $items) );
-                       $log->addEntry( 'event', $title, $reason, $params );
-               } else {
-                       // Add params for effected page and ids
-                       $params = array( $param, implode( ',', $items) );
-                       $log->addEntry( 'revision', $title, $reason, $params );
-               }
-       }
-}
diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php
deleted file mode 100644 (file)
index 0a483af..0000000
+++ /dev/null
@@ -1,651 +0,0 @@
-<?php
-# Copyright (C) 2004 Brion Vibber <brion@pobox.com>
-# http://www.mediawiki.org/
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-# http://www.gnu.org/copyleft/gpl.html
-
-/**
- * Run text & title search and display the output
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point
- *
- * @param $par String: (default '')
- */
-function wfSpecialSearch( $par = '' ) {
-       global $wgRequest, $wgUser;
-
-       $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
-       $searchPage = new SpecialSearch( $wgRequest, $wgUser );
-       if( $wgRequest->getVal( 'fulltext' ) 
-               || !is_null( $wgRequest->getVal( 'offset' )) 
-               || !is_null( $wgRequest->getVal( 'searchx' ))) {
-               $searchPage->showResults( $search, 'search' );
-       } else {
-               $searchPage->goResult( $search );
-       }
-}
-
-/**
- * implements Special:Search - Run text & title search and display the output
- * @ingroup SpecialPage
- */
-class SpecialSearch {
-
-       /**
-        * Set up basic search parameters from the request and user settings.
-        * Typically you'll pass $wgRequest and $wgUser.
-        *
-        * @param WebRequest $request
-        * @param User $user
-        * @public
-        */
-       function SpecialSearch( &$request, &$user ) {
-               list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
-
-               $this->namespaces = $this->powerSearch( $request );
-               if( empty( $this->namespaces ) ) {
-                       $this->namespaces = SearchEngine::userNamespaces( $user );
-               }
-
-               $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
-       }
-
-       /**
-        * If an exact title match can be found, jump straight ahead to it.
-        * @param string $term
-        * @public
-        */
-       function goResult( $term ) {
-               global $wgOut;
-               global $wgGoToEdit;
-
-               $this->setupPage( $term );
-
-               # Try to go to page as entered.
-               $t = Title::newFromText( $term );
-
-               # If the string cannot be used to create a title
-               if( is_null( $t ) ){
-                       return $this->showResults( $term );
-               }
-
-               # If there's an exact or very near match, jump right there.
-               $t = SearchEngine::getNearMatch( $term );
-               if( !is_null( $t ) ) {
-                       $wgOut->redirect( $t->getFullURL() );
-                       return;
-               }
-
-               # No match, generate an edit URL
-               $t = Title::newFromText( $term );
-               if( ! is_null( $t ) ) {
-                       wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
-                       # If the feature is enabled, go straight to the edit page
-                       if ( $wgGoToEdit ) {
-                               $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
-                               return;
-                       }
-               }
-
-               $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' );
-               if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
-                       $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) );
-               } else {
-                       $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) );
-               }
-
-               return $this->showResults( $term );
-       }
-
-       /**
-        * @param string $term
-        * @public
-        */
-       function showResults( $term ) {
-               $fname = 'SpecialSearch::showResults';
-               wfProfileIn( $fname );
-               global $wgOut, $wgUser;
-               $sk = $wgUser->getSkin();
-
-               $this->setupPage( $term );
-
-               $wgOut->addWikiMsg( 'searchresulttext' );
-
-               if( '' === trim( $term ) ) {
-                       // Empty query -- straight view of search form
-                       $wgOut->setSubtitle( '' );
-                       $wgOut->addHTML( $this->powerSearchBox( $term ) );
-                       $wgOut->addHTML( $this->powerSearchFocus() );
-                       wfProfileOut( $fname );
-                       return;
-               }
-
-               global $wgDisableTextSearch;
-               if ( $wgDisableTextSearch ) {
-                       global $wgForwardSearchUrl;
-                       if( $wgForwardSearchUrl ) {
-                               $url = str_replace( '$1', urlencode( $term ), $wgForwardSearchUrl );
-                               $wgOut->redirect( $url );
-                               return;
-                       }
-                       global $wgInputEncoding;
-                       $wgOut->addHTML(
-                               Xml::openElement( 'fieldset' ) .
-                               Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
-                               Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
-                               wfMsg( 'googlesearch',
-                                       htmlspecialchars( $term ),
-                                       htmlspecialchars( $wgInputEncoding ),
-                                       htmlspecialchars( wfMsg( 'searchbutton' ) )
-                               ) .
-                               Xml::closeElement( 'fieldset' )
-                       );
-                       wfProfileOut( $fname );
-                       return;
-               }
-
-               $wgOut->addHTML( $this->shortDialog( $term ) );
-
-               $search = SearchEngine::create();
-               $search->setLimitOffset( $this->limit, $this->offset );
-               $search->setNamespaces( $this->namespaces );
-               $search->showRedirects = $this->searchRedirects;
-               $rewritten = $search->replacePrefixes($term);
-
-               $titleMatches = $search->searchTitle( $rewritten );
-
-               // Sometimes the search engine knows there are too many hits
-               if ($titleMatches instanceof SearchResultTooMany) {
-                       $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
-                       $wgOut->addHTML( $this->powerSearchBox( $term ) );
-                       $wgOut->addHTML( $this->powerSearchFocus() );
-                       wfProfileOut( $fname );
-                       return;
-               }
-               
-               $textMatches = $search->searchText( $rewritten );
-
-               // did you mean... suggestions
-               if($textMatches && $textMatches->hasSuggestion()){
-                       $st = SpecialPage::getTitleFor( 'Search' );                     
-                       $stParams = wfArrayToCGI( array( 
-                                       'search'        => $textMatches->getSuggestionQuery(), 
-                                       'fulltext'      => wfMsg('search')),
-                                       $this->powerSearchOptions());
-                                       
-                       $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
-                                       $textMatches->getSuggestionSnippet().'</a>';
-                                       
-                       $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
-               }
-
-               // show number of results
-               $num = ( $titleMatches ? $titleMatches->numRows() : 0 )
-                       + ( $textMatches ? $textMatches->numRows() : 0);
-               $totalNum = 0;
-               if($titleMatches && !is_null($titleMatches->getTotalHits()))
-                       $totalNum += $titleMatches->getTotalHits();
-               if($textMatches && !is_null($textMatches->getTotalHits()))
-                       $totalNum += $textMatches->getTotalHits();
-               if ( $num > 0 ) {
-                       if ( $totalNum > 0 ){
-                               $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), 
-                                       $this->offset+1, $this->offset+$num, $totalNum );
-                       } elseif ( $num >= $this->limit ) {
-                               $top = wfShowingResults( $this->offset, $this->limit );
-                       } else {
-                               $top = wfShowingResultsNum( $this->offset, $this->limit, $num );
-                       }
-                       $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
-               }
-
-               // prev/next links
-               if( $num || $this->offset ) {
-                       $prevnext = wfViewPrevNext( $this->offset, $this->limit,
-                               SpecialPage::getTitleFor( 'Search' ),
-                               wfArrayToCGI(
-                                       $this->powerSearchOptions(),
-                                       array( 'search' => $term ) ),
-                                       ($num < $this->limit) );
-                       $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
-                       wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
-               } else {
-                       wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
-               }
-
-               if( $titleMatches ) {
-                       if( $titleMatches->numRows() ) {
-                               $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
-                               $wgOut->addHTML( $this->showMatches( $titleMatches ) );
-                       }
-                       $titleMatches->free();
-               }
-
-               if( $textMatches ) {
-                       // output appropriate heading
-                       if( $textMatches->numRows() ) {
-                               if($titleMatches)
-                                       $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
-                               else // if no title matches the heading is redundant
-                                       $wgOut->addHTML("<hr/>");                                                               
-                       } elseif( $num == 0 ) {
-                               # Don't show the 'no text matches' if we received title matches
-                               $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
-                       }
-                       // show interwiki results if any
-                       if( $textMatches->hasInterwikiResults() )
-                               $wgOut->addHtml( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
-                       // show results
-                       if( $textMatches->numRows() )
-                               $wgOut->addHTML( $this->showMatches( $textMatches ) );
-
-                       $textMatches->free();
-               }
-
-               if ( $num == 0 ) {
-                       $wgOut->addWikiMsg( 'nonefound' );
-               }
-               if( $num || $this->offset ) {
-                       $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
-               }
-               $wgOut->addHTML( $this->powerSearchBox( $term ) );
-               wfProfileOut( $fname );
-       }
-
-       #------------------------------------------------------------------
-       # Private methods below this line
-       
-       /**
-        *
-        */
-       function setupPage( $term ) {
-               global $wgOut;
-               if( !empty( $term ) )
-                       $wgOut->setPageTitle( wfMsg( 'searchresults' ) );                       
-               $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
-               $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
-               $wgOut->setArticleRelated( false );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-       }
-
-       /**
-        * Extract "power search" namespace settings from the request object,
-        * returning a list of index numbers to search.
-        *
-        * @param WebRequest $request
-        * @return array
-        * @private
-        */
-       function powerSearch( &$request ) {
-               $arr = array();
-               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
-                       if( $request->getCheck( 'ns' . $ns ) ) {
-                               $arr[] = $ns;
-                       }
-               }
-               return $arr;
-       }
-
-       /**
-        * Reconstruct the 'power search' options for links
-        * @return array
-        * @private
-        */
-       function powerSearchOptions() {
-               $opt = array();
-               foreach( $this->namespaces as $n ) {
-                       $opt['ns' . $n] = 1;
-               }
-               $opt['redirs'] = $this->searchRedirects ? 1 : 0;
-               return $opt;
-       }
-
-       /**
-        * Show whole set of results 
-        * 
-        * @param SearchResultSet $matches
-        */
-       function showMatches( &$matches ) {
-               $fname = 'SpecialSearch::showMatches';
-               wfProfileIn( $fname );
-
-               global $wgContLang;
-               $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
-
-               $out = "";
-               
-               $infoLine = $matches->getInfo();
-               if( !is_null($infoLine) )
-                       $out .= "\n<!-- {$infoLine} -->\n";
-                       
-               
-               $off = $this->offset + 1;
-               $out .= "<ul class='mw-search-results'>\n";
-
-               while( $result = $matches->next() ) {
-                       $out .= $this->showHit( $result, $terms );
-               }
-               $out .= "</ul>\n";
-
-               // convert the whole thing to desired language variant
-               global $wgContLang;
-               $out = $wgContLang->convert( $out );
-               wfProfileOut( $fname );
-               return $out;
-       }
-
-       /**
-        * Format a single hit result
-        * @param SearchResult $result
-        * @param array $terms terms to highlight
-        */
-       function showHit( $result, $terms ) {
-               $fname = 'SpecialSearch::showHit';
-               wfProfileIn( $fname );
-               global $wgUser, $wgContLang, $wgLang;
-               
-               if( $result->isBrokenTitle() ) {
-                       wfProfileOut( $fname );
-                       return "<!-- Broken link in search result -->\n";
-               }
-               
-               $t = $result->getTitle();
-               $sk = $wgUser->getSkin();
-
-               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
-
-               //If page content is not readable, just return the title.
-               //This is not quite safe, but better than showing excerpts from non-readable pages
-               //Note that hiding the entry entirely would screw up paging.
-               if (!$t->userCanRead()) {
-                       wfProfileOut( $fname );
-                       return "<li>{$link}</li>\n";
-               }
-
-               // If the page doesn't *exist*... our search index is out of date.
-               // The least confusing at this point is to drop the result.
-               // You may get less results, but... oh well. :P
-               if( $result->isMissingRevision() ) {
-                       wfProfileOut( $fname );
-                       return "<!-- missing page " .
-                               htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
-               }
-
-               // format redirects / relevant sections
-               $redirectTitle = $result->getRedirectTitle();
-               $redirectText = $result->getRedirectSnippet($terms);
-               $sectionTitle = $result->getSectionTitle();
-               $sectionText = $result->getSectionSnippet($terms);
-               $redirect = '';
-               if( !is_null($redirectTitle) )
-                       $redirect = "<span class='searchalttitle'>"
-                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
-                               ."</span>";
-               $section = '';
-               if( !is_null($sectionTitle) )
-                       $section = "<span class='searchalttitle'>" 
-                               .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
-                               ."</span>";
-
-               // format text extract
-               $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
-               
-               // format score
-               if( is_null( $result->getScore() ) ) {
-                       // Search engine doesn't report scoring info
-                       $score = '';
-               } else {
-                       $percent = sprintf( '%2.1f', $result->getScore() * 100 );
-                       $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
-                               . ' - ';
-               }
-
-               // format description
-               $byteSize = $result->getByteSize();
-               $wordCount = $result->getWordCount();
-               $timestamp = $result->getTimestamp();
-               $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
-                       $sk->formatSize( $byteSize ),
-                       $wordCount );
-               $date = $wgLang->timeanddate( $timestamp );
-
-               // link to related articles if supported
-               $related = '';
-               if( $result->hasRelated() ){
-                       $st = SpecialPage::getTitleFor( 'Search' );
-                       $stParams = wfArrayToCGI( $this->powerSearchOptions(),
-                               array('search'    => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
-                                     'fulltext'  => wfMsg('search') ));
-                       
-                       $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'. 
-                               wfMsg('search-relatedarticle').'</a>';
-               }
-                               
-               // Include a thumbnail for media files...
-               if( $t->getNamespace() == NS_IMAGE ) {
-                       $img = wfFindFile( $t );
-                       if( $img ) {
-                               $thumb = $img->getThumbnail( 120, 120 );
-                               if( $thumb ) {
-                                       $desc = $img->getShortDesc();
-                                       wfProfileOut( $fname );
-                                       // Ugly table. :D
-                                       // Float doesn't seem to interact well with the bullets.
-                                       // Table messes up vertical alignment of the bullet, but I'm
-                                       // not sure what more I can do about that. :(
-                                       return "<li>" .
-                                               '<table class="searchResultImage">' .
-                                               '<tr>' .
-                                               '<td width="120" align="center">' .
-                                               $thumb->toHtml( array( 'desc-link' => true ) ) .
-                                               '</td>' .
-                                               '<td valign="top">' .
-                                               $link .
-                                               $extract .
-                                               "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
-                                               '</td>' .
-                                               '</tr>' .
-                                               '</table>' .
-                                               "</li>\n";
-                               }
-                       }
-               }
-
-               wfProfileOut( $fname );
-               return "<li>{$link} {$redirect} {$section} {$extract}\n" .
-                       "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
-                       "</li>\n";
-
-       }
-
-       /**
-        * Show results from other wikis
-        * 
-        * @param SearchResultSet $matches
-        */
-       function showInterwiki( &$matches, $query ) {
-               $fname = 'SpecialSearch::showInterwiki';
-               wfProfileIn( $fname );
-
-               global $wgContLang;
-               $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
-
-               $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".wfMsg('search-interwiki-caption')."</div>\n";             
-               $off = $this->offset + 1;
-               $out .= "<ul start='{$off}' class='mw-search-iwresults'>\n";
-
-               // work out custom project captions
-               $customCaptions = array();
-               $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
-               foreach($customLines as $line){
-                       $parts = explode(":",$line,2);
-                       if(count($parts) == 2) // validate line
-                               $customCaptions[$parts[0]] = $parts[1]; 
-               }
-               
-               
-               $prev = null;
-               while( $result = $matches->next() ) {
-                       $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
-                       $prev = $result->getInterwikiPrefix();
-               }
-               // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
-               $out .= "</ul></div>\n";
-
-               // convert the whole thing to desired language variant
-               global $wgContLang;
-               $out = $wgContLang->convert( $out );
-               wfProfileOut( $fname );
-               return $out;
-       }
-       
-       /**
-        * Show single interwiki link
-        *
-        * @param SearchResult $result
-        * @param string $lastInterwiki
-        * @param array $terms
-        * @param string $query 
-        * @param array $customCaptions iw prefix -> caption
-        */
-       function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
-               $fname = 'SpecialSearch::showInterwikiHit';
-               wfProfileIn( $fname );
-               global $wgUser, $wgContLang, $wgLang;
-               
-               if( $result->isBrokenTitle() ) {
-                       wfProfileOut( $fname );
-                       return "<!-- Broken link in search result -->\n";
-               }
-               
-               $t = $result->getTitle();
-               $sk = $wgUser->getSkin();
-               
-               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
-                               
-               // format redirect if any
-               $redirectTitle = $result->getRedirectTitle();
-               $redirectText = $result->getRedirectSnippet($terms);
-               $redirect = '';
-               if( !is_null($redirectTitle) )
-                       $redirect = "<span class='searchalttitle'>"
-                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
-                               ."</span>";
-
-               $out = "";
-               // display project name 
-               if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
-                       if( key_exists($t->getInterwiki(),$customCaptions) )
-                               // captions from 'search-interwiki-custom'
-                               $caption = $customCaptions[$t->getInterwiki()];
-                       else{
-                               // default is to show the hostname of the other wiki which might suck 
-                               // if there are many wikis on one hostname
-                               $parsed = parse_url($t->getFullURL());
-                               $caption = wfMsg('search-interwiki-default', $parsed['host']); 
-                       }               
-                       // "more results" link (special page stuff could be localized, but we might not know target lang)
-                       $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");                        
-                       $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
-                               wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); 
-                       $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>{$searchLink}</span>{$caption}</div>\n<ul>";
-               }
-
-               $out .= "<li>{$link} {$redirect}</li>\n"; 
-               wfProfileOut( $fname );
-               return $out;
-       }
-       
-
-       /**
-        * Generates the power search box at bottom of [[Special:Search]]
-        * @param $term string: search term
-        * @return $out string: HTML form
-        */
-       function powerSearchBox( $term ) {
-               global $wgScript;
-
-               $namespaces = '';
-               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
-                       $name = str_replace( '_', ' ', $name );
-                       if( '' == $name ) {
-                               $name = wfMsg( 'blanknamespace' );
-                       }
-                       $namespaces .= Xml::openElement( 'span', array( 'style' => 'white-space: nowrap' ) ) .
-                                       Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
-                                       Xml::closeElement( 'span' ) . "\n";
-               }
-
-               $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
-               $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
-               $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
-               $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n";
-
-               $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
-                       Xml::fieldset( wfMsg( 'powersearch-legend' ),
-                               Xml::hidden( 'title', 'Special:Search' ) .
-                               "<p>" .
-                               wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
-                               "<br />" .
-                               $namespaces .
-                               "</p>" .
-                               "<p>" .
-                               $redirect . " " . $redirectLabel .
-                               "</p>" .
-                               wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
-                               "&nbsp;" .
-                               $searchField .
-                               "&nbsp;" .
-                               $searchButton ) .
-                       "</form>";
-
-               return $out;
-       }
-
-       function powerSearchFocus() {
-               global $wgJsMimeType;
-               return "<script type=\"$wgJsMimeType\">" .
-                       "hookEvent(\"load\", function(){" .
-                               "document.getElementById('powerSearchText').focus();" .
-                       "});" .
-                       "</script>";
-       }
-
-       function shortDialog($term) {
-               global $wgScript;
-
-               $out  = Xml::openElement( 'form', array(
-                       'id' => 'search',
-                       'method' => 'get',
-                       'action' => $wgScript
-               ));
-               $out .= Xml::hidden( 'title', 'Special:Search' );
-               $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
-               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
-                       if( in_array( $ns, $this->namespaces ) ) {
-                               $out .= Xml::hidden( "ns{$ns}", '1' );
-                       }
-               }
-               $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
-               $out .= Xml::closeElement( 'form' );
-
-               return $out;
-       }
-}
diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php
deleted file mode 100644 (file)
index 2e7d24a..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * SpecialShortpages extends QueryPage. It is used to return the shortest
- * pages in the database.
- * @ingroup SpecialPage
- */
-class ShortPagesPage extends QueryPage {
-
-       function getName() {
-               return 'Shortpages';
-       }
-
-       /**
-        * This query is indexed as of 1.5
-        */
-       function isExpensive() {
-               return true;
-       }
-
-       function isSyndicated() {
-               return false;
-       }
-
-       function getSQL() {
-               global $wgContentNamespaces;
-
-               $dbr = wfGetDB( DB_SLAVE );
-               $page = $dbr->tableName( 'page' );
-               $name = $dbr->addQuotes( $this->getName() );
-
-               $forceindex = $dbr->useIndexClause("page_len");
-
-               if ($wgContentNamespaces)
-                       $nsclause = "page_namespace IN (" . $dbr->makeList($wgContentNamespaces) . ")";
-               else
-                       $nsclause = "page_namespace = " . NS_MAIN;
-
-               return
-                       "SELECT $name as type,
-                               page_namespace as namespace,
-                               page_title as title,
-                               page_len AS value
-                       FROM $page $forceindex
-                       WHERE $nsclause AND page_is_redirect=0";
-       }
-
-       function preprocessResults( $db, $res ) {
-               # There's no point doing a batch check if we aren't caching results;
-               # the page must exist for it to have been pulled out of the table
-               if( $this->isCached() ) {
-                       $batch = new LinkBatch();
-                       while( $row = $db->fetchObject( $res ) )
-                               $batch->add( $row->namespace, $row->title );
-                       $batch->execute();
-                       if( $db->numRows( $res ) > 0 )
-                               $db->dataSeek( $res, 0 );
-               }
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-               $dm = $wgContLang->getDirMark();
-
-               $title = Title::makeTitleSafe( $result->namespace, $result->title );
-               if ( !$title ) {
-                       return '<!-- Invalid title ' .  htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->';
-               }
-               $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
-               $plink = $this->isCached()
-                                       ? $skin->makeLinkObj( $title )
-                                       : $skin->makeKnownLinkObj( $title );
-               $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) );
-
-               return $title->exists()
-                               ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"
-                               : "<s>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</s>";
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialShortpages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $spp = new ShortPagesPage();
-
-       return $spp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php
deleted file mode 100644 (file)
index ca91ad5..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-function wfSpecialSpecialpages() {
-       global $wgOut, $wgUser, $wgMessageCache, $wgSortSpecialPages;
-
-       $wgMessageCache->loadAllMessages();
-
-       $wgOut->setRobotpolicy( 'noindex,nofollow' );  # Is this really needed?
-       $sk = $wgUser->getSkin();
-
-       $pages = SpecialPage::getUsablePages();
-
-       if( count( $pages ) == 0 ) {
-               # Yeah, that was pointless. Thanks for coming.
-               return;
-       }
-
-       /** Put them into a sortable array */
-       $groups = array();
-       foreach ( $pages as $page ) {
-               if ( $page->isListed() ) {
-                       $group = SpecialPage::getGroup( $page );
-                       if( !isset($groups[$group]) ) {
-                               $groups[$group] = array();
-                       }
-                       $groups[$group][$page->getDescription()] = array( $page->getTitle(), $page->isRestricted() );
-               }
-       }
-
-       /** Sort */
-       if ( $wgSortSpecialPages ) {
-               foreach( $groups as $group => $sortedPages ) {
-                       ksort( $groups[$group] );
-               }
-       }
-
-       /** Always move "other" to end */
-       if( array_key_exists('other',$groups) ) {
-               $other = $groups['other'];
-               unset( $groups['other'] );
-               $groups['other'] = $other;
-       }
-
-       /** Now output the HTML */
-       foreach ( $groups as $group => $sortedPages ) {
-               $middle = ceil( count($sortedPages)/2 );
-               $total = count($sortedPages);
-               $count = 0;
-
-               $wgOut->addHTML( "<h4 class='mw-specialpagesgroup'>".wfMsgHtml("specialpages-group-$group")."</h4>\n" );
-               $wgOut->addHTML( "<table style='width: 100%;' class='mw-specialpages-table'><tr>" );
-               $wgOut->addHTML( "<td width='30%' valign='top'><ul>\n" );
-               foreach( $sortedPages as $desc => $specialpage ) {
-                       list( $title, $restricted ) = $specialpage;
-                       $link = $sk->makeKnownLinkObj( $title , htmlspecialchars( $desc ) );
-                       if( $restricted ) {
-                               $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'>{$link}</li>\n" );
-                       } else {
-                               $wgOut->addHTML( "<li>{$link}</li>\n" );
-                       }
-
-                       # Split up the larger groups
-                       $count++;
-                       if( $total > 3 && $count == $middle ) {
-                               $wgOut->addHTML( "</ul></td><td width='10%'></td><td width='30%' valign='top'><ul>" );
-                       }
-               }
-               $wgOut->addHTML( "</ul></td><td width='30%' valign='top'></td></tr></table>\n" );
-       }
-       $wgOut->addHTML(
-               Xml::openElement('div', array( 'class' => 'mw-specialpages-notes' )).
-               wfMsgWikiHtml('specialpages-note').
-               Xml::closeElement('div')
-       );
-}
diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php
deleted file mode 100644 (file)
index 570a21c..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<?php
-
-/**
- * Special page lists various statistics, including the contents of
- * `site_stats`, plus page view details if enabled
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Show the special page
- *
- * @param mixed $par (not used)
- */
-function wfSpecialStatistics( $par = '' ) {
-       global $wgOut, $wgLang, $wgRequest;
-       $dbr = wfGetDB( DB_SLAVE );
-
-       $views = SiteStats::views();
-       $edits = SiteStats::edits();
-       $good = SiteStats::articles();
-       $images = SiteStats::images();
-       $total = SiteStats::pages();
-       $users = SiteStats::users();
-       $admins = SiteStats::admins();
-       $numJobs = SiteStats::jobs();
-
-       if( $wgRequest->getVal( 'action' ) == 'raw' ) {
-               $wgOut->disable();
-               header( 'Pragma: nocache' );
-               echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n";
-               return;
-       } else {
-               $text = "__NOTOC__\n";
-               $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n";
-               $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ),
-                       $wgLang->formatNum( $total ),
-                       $wgLang->formatNum( $good ),
-                       $wgLang->formatNum( $views ),
-                       $wgLang->formatNum( $edits ),
-                       $wgLang->formatNum( sprintf( '%.2f', $total ? $edits / $total : 0 ) ),
-                       $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ),
-                       $wgLang->formatNum( $numJobs ),
-                       $wgLang->formatNum( $images )
-               )."\n";
-
-               $text .= "==" . wfMsgNoTrans( 'userstats' ) . "==\n";
-               $text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ),
-                       $wgLang->formatNum( $users ),
-                       $wgLang->formatNum( $admins ),
-                       '[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility
-                       $wgLang->formatNum( @sprintf( '%.2f', $admins / $users * 100 ) ),
-                       User::makeGroupLinkWiki( 'sysop' )
-               )."\n";
-
-               global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang;
-               if( !$wgDisableCounters && !$wgMiserMode ) {
-                       $res = $dbr->select(
-                               'page',
-                               array(
-                                       'page_namespace',
-                                       'page_title',
-                                       'page_counter',
-                               ),
-                               array(
-                                       'page_is_redirect' => 0,
-                                       'page_counter > 0',
-                               ),
-                               __METHOD__,
-                               array(
-                                       'ORDER BY' => 'page_counter DESC',
-                                       'LIMIT' => 10,
-                               )
-                       );
-                       if( $res->numRows() > 0 ) {
-                               $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n";
-                               while( $row = $res->fetchObject() ) {
-                                       $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
-                                       if( $title instanceof Title )
-                                               $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n";
-                               }
-                               $res->free();
-                       }
-               }
-
-               $footer = wfMsgNoTrans( 'statistics-footer' );
-               if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
-                       $text .= "\n" . $footer;
-
-               $wgOut->addWikiText( $text );
-       }
-}
diff --git a/includes/SpecialUncategorizedcategories.php b/includes/SpecialUncategorizedcategories.php
deleted file mode 100644 (file)
index a178712..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-require_once( "SpecialUncategorizedpages.php" );
-
-/**
- * implements Special:Uncategorizedcategories
- * @ingroup SpecialPage
- */
-class UncategorizedCategoriesPage extends UncategorizedPagesPage {
-       function UncategorizedCategoriesPage() {
-               $this->requestedNamespace = NS_CATEGORY;
-       }
-
-       function getName() {
-               return "Uncategorizedcategories";
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialUncategorizedcategories() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $lpp = new UncategorizedCategoriesPage();
-
-       return $lpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUncategorizedimages.php b/includes/SpecialUncategorizedimages.php
deleted file mode 100644 (file)
index 986ec96..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-/**
- * Special page lists images which haven't been categorised
- *
- * @file
- * @ingroup SpecialPage
- * @author Rob Church <robchur@gmail.com>
- */
-
-/**
- * @ingroup SpecialPage
- */
-class UncategorizedImagesPage extends ImageQueryPage {
-
-       function getName() {
-               return 'Uncategorizedimages';
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function isExpensive() {
-               return true;
-       }
-
-       function isSyndicated() {
-               return false;
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
-               $ns = NS_IMAGE;
-
-               return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace,
-                               page_title AS title, page_title AS value
-                               FROM {$page} LEFT JOIN {$categorylinks} ON page_id = cl_from
-                               WHERE cl_from IS NULL AND page_namespace = {$ns} AND page_is_redirect = 0";
-       }
-
-}
-
-function wfSpecialUncategorizedimages() {
-       $uip = new UncategorizedImagesPage();
-       list( $limit, $offset ) = wfCheckLimits();
-       return $uip->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUncategorizedpages.php b/includes/SpecialUncategorizedpages.php
deleted file mode 100644 (file)
index e7f0aac..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page looking for page without any category.
- * @ingroup SpecialPage
- */
-class UncategorizedPagesPage extends PageQueryPage {
-       var $requestedNamespace = NS_MAIN;
-
-       function getName() {
-               return "Uncategorizedpages";
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function isExpensive() {
-               return true;
-       }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
-               $name = $dbr->addQuotes( $this->getName() );
-
-               return
-                       "
-                       SELECT
-                               $name as type,
-                               page_namespace AS namespace,
-                               page_title AS title,
-                               page_title AS value
-                       FROM $page
-                       LEFT JOIN $categorylinks ON page_id=cl_from
-                       WHERE cl_from IS NULL AND page_namespace={$this->requestedNamespace} AND page_is_redirect=0
-                       ";
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialUncategorizedpages() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $lpp = new UncategorizedPagesPage();
-
-       return $lpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUncategorizedtemplates.php b/includes/SpecialUncategorizedtemplates.php
deleted file mode 100644 (file)
index cb2a6d4..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Special page lists all uncategorised pages in the
- * template namespace
- *
- * @ingroup SpecialPage
- * @author Rob Church <robchur@gmail.com>
- */
-class UncategorizedTemplatesPage extends UncategorizedPagesPage {
-
-       var $requestedNamespace = NS_TEMPLATE;
-
-       public function getName() {
-               return 'Uncategorizedtemplates';
-       }
-
-}
-
-/**
- * Main execution point
- *
- * @param mixed $par Parameter passed to the page
- */
-function wfSpecialUncategorizedtemplates() {
-       list( $limit, $offset ) = wfCheckLimits();
-       $utp = new UncategorizedTemplatesPage();
-       $utp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php
deleted file mode 100644 (file)
index 33d9476..0000000
+++ /dev/null
@@ -1,1278 +0,0 @@
-<?php
-
-/**
- * Special page allowing users with the appropriate permissions to view
- * and restore deleted content
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Constructor
- */
-function wfSpecialUndelete( $par ) {
-       global $wgRequest;
-
-       $form = new UndeleteForm( $wgRequest, $par );
-       $form->execute();
-}
-
-/**
- * Used to show archived pages and eventually restore them.
- * @ingroup SpecialPage
- */
-class PageArchive {
-       protected $title;
-       var $fileStatus;
-
-       function __construct( $title ) {
-               if( is_null( $title ) ) {
-                       throw new MWException( 'Archiver() given a null title.');
-               }
-               $this->title = $title;
-       }
-
-       /**
-        * List all deleted pages recorded in the archive table. Returns result
-        * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
-        * namespace/title.
-        *
-        * @return ResultWrapper
-        */
-       public static function listAllPages() {
-               $dbr = wfGetDB( DB_SLAVE );
-               return self::listPages( $dbr, '' );
-       }
-
-       /**
-        * List deleted pages recorded in the archive table matching the
-        * given title prefix.
-        * Returns result wrapper with (ar_namespace, ar_title, count) fields.
-        *
-        * @return ResultWrapper
-        */
-       public static function listPagesByPrefix( $prefix ) {
-               $dbr = wfGetDB( DB_SLAVE );
-
-               $title = Title::newFromText( $prefix );
-               if( $title ) {
-                       $ns = $title->getNamespace();
-                       $encPrefix = $dbr->escapeLike( $title->getDBkey() );
-               } else {
-                       // Prolly won't work too good
-                       // @todo handle bare namespace names cleanly?
-                       $ns = 0;
-                       $encPrefix = $dbr->escapeLike( $prefix );
-               }
-               $conds = array(
-                       'ar_namespace' => $ns,
-                       "ar_title LIKE '$encPrefix%'",
-               );
-               return self::listPages( $dbr, $conds );
-       }
-
-       protected static function listPages( $dbr, $condition ) {
-               return $dbr->resultObject(
-                       $dbr->select(
-                               array( 'archive' ),
-                               array(
-                                       'ar_namespace',
-                                       'ar_title',
-                                       'COUNT(*) AS count'
-                               ),
-                               $condition,
-                               __METHOD__,
-                               array(
-                                       'GROUP BY' => 'ar_namespace,ar_title',
-                                       'ORDER BY' => 'ar_namespace,ar_title',
-                                       'LIMIT' => 100,
-                               )
-                       )
-               );
-       }
-
-       /**
-        * List the revisions of the given page. Returns result wrapper with
-        * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
-        *
-        * @return ResultWrapper
-        */
-       function listRevisions() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select( 'archive',
-                       array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted' ),
-                       array( 'ar_namespace' => $this->title->getNamespace(),
-                              'ar_title' => $this->title->getDBkey() ),
-                       'PageArchive::listRevisions',
-                       array( 'ORDER BY' => 'ar_timestamp DESC' ) );
-               $ret = $dbr->resultObject( $res );
-               return $ret;
-       }
-
-       /**
-        * List the deleted file revisions for this page, if it's a file page.
-        * Returns a result wrapper with various filearchive fields, or null
-        * if not a file page.
-        *
-        * @return ResultWrapper
-        * @todo Does this belong in Image for fuller encapsulation?
-        */
-       function listFiles() {
-               if( $this->title->getNamespace() == NS_IMAGE ) {
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $res = $dbr->select( 'filearchive',
-                               array(
-                                       'fa_id',
-                                       'fa_name',
-                                       'fa_archive_name',
-                                       'fa_storage_key',
-                                       'fa_storage_group',
-                                       'fa_size',
-                                       'fa_width',
-                                       'fa_height',
-                                       'fa_bits',
-                                       'fa_metadata',
-                                       'fa_media_type',
-                                       'fa_major_mime',
-                                       'fa_minor_mime',
-                                       'fa_description',
-                                       'fa_user',
-                                       'fa_user_text',
-                                       'fa_timestamp',
-                                       'fa_deleted' ),
-                               array( 'fa_name' => $this->title->getDBkey() ),
-                               __METHOD__,
-                               array( 'ORDER BY' => 'fa_timestamp DESC' ) );
-                       $ret = $dbr->resultObject( $res );
-                       return $ret;
-               }
-               return null;
-       }
-
-       /**
-        * Fetch (and decompress if necessary) the stored text for the deleted
-        * revision of the page with the given timestamp.
-        *
-        * @return string
-        * @deprecated Use getRevision() for more flexible information
-        */
-       function getRevisionText( $timestamp ) {
-               $rev = $this->getRevision( $timestamp );
-               return $rev ? $rev->getText() : null;
-       }
-
-       /**
-        * Return a Revision object containing data for the deleted revision.
-        * Note that the result *may* or *may not* have a null page ID.
-        * @param string $timestamp
-        * @return Revision
-        */
-       function getRevision( $timestamp ) {
-               $dbr = wfGetDB( DB_SLAVE );
-               $row = $dbr->selectRow( 'archive',
-                       array(
-                               'ar_rev_id',
-                               'ar_text',
-                               'ar_comment',
-                               'ar_user',
-                               'ar_user_text',
-                               'ar_timestamp',
-                               'ar_minor_edit',
-                               'ar_flags',
-                               'ar_text_id',
-                               'ar_deleted',
-                               'ar_len' ),
-                       array( 'ar_namespace' => $this->title->getNamespace(),
-                              'ar_title' => $this->title->getDBkey(),
-                              'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
-                       __METHOD__ );
-               if( $row ) {
-                       return new Revision( array(
-                               'page'       => $this->title->getArticleId(),
-                               'id'         => $row->ar_rev_id,
-                               'text'       => ($row->ar_text_id
-                                       ? null
-                                       : Revision::getRevisionText( $row, 'ar_' ) ),
-                               'comment'    => $row->ar_comment,
-                               'user'       => $row->ar_user,
-                               'user_text'  => $row->ar_user_text,
-                               'timestamp'  => $row->ar_timestamp,
-                               'minor_edit' => $row->ar_minor_edit,
-                               'text_id'    => $row->ar_text_id,
-                               'deleted'    => $row->ar_deleted,
-                               'len'        => $row->ar_len) );
-               } else {
-                       return null;
-               }
-       }
-
-       /**
-        * Return the most-previous revision, either live or deleted, against
-        * the deleted revision given by timestamp.
-        *
-        * May produce unexpected results in case of history merges or other
-        * unusual time issues.
-        *
-        * @param string $timestamp
-        * @return Revision or null
-        */
-       function getPreviousRevision( $timestamp ) {
-               $dbr = wfGetDB( DB_SLAVE );
-
-               // Check the previous deleted revision...
-               $row = $dbr->selectRow( 'archive',
-                       'ar_timestamp',
-                       array( 'ar_namespace' => $this->title->getNamespace(),
-                              'ar_title' => $this->title->getDBkey(),
-                              'ar_timestamp < ' .
-                                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
-                       __METHOD__,
-                       array(
-                               'ORDER BY' => 'ar_timestamp DESC',
-                               'LIMIT' => 1 ) );
-               $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
-
-               $row = $dbr->selectRow( array( 'page', 'revision' ),
-                       array( 'rev_id', 'rev_timestamp' ),
-                       array(
-                               'page_namespace' => $this->title->getNamespace(),
-                               'page_title' => $this->title->getDBkey(),
-                               'page_id = rev_page',
-                               'rev_timestamp < ' .
-                                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
-                       __METHOD__,
-                       array(
-                               'ORDER BY' => 'rev_timestamp DESC',
-                               'LIMIT' => 1 ) );
-               $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
-               $prevLiveId = $row ? intval( $row->rev_id ) : null;
-
-               if( $prevLive && $prevLive > $prevDeleted ) {
-                       // Most prior revision was live
-                       return Revision::newFromId( $prevLiveId );
-               } elseif( $prevDeleted ) {
-                       // Most prior revision was deleted
-                       return $this->getRevision( $prevDeleted );
-               } else {
-                       // No prior revision on this page.
-                       return null;
-               }
-       }
-
-       /**
-        * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
-        */
-       function getTextFromRow( $row ) {
-               if( is_null( $row->ar_text_id ) ) {
-                       // An old row from MediaWiki 1.4 or previous.
-                       // Text is embedded in this row in classic compression format.
-                       return Revision::getRevisionText( $row, "ar_" );
-               } else {
-                       // New-style: keyed to the text storage backend.
-                       $dbr = wfGetDB( DB_SLAVE );
-                       $text = $dbr->selectRow( 'text',
-                               array( 'old_text', 'old_flags' ),
-                               array( 'old_id' => $row->ar_text_id ),
-                               __METHOD__ );
-                       return Revision::getRevisionText( $text );
-               }
-       }
-
-
-       /**
-        * Fetch (and decompress if necessary) the stored text of the most
-        * recently edited deleted revision of the page.
-        *
-        * If there are no archived revisions for the page, returns NULL.
-        *
-        * @return string
-        */
-       function getLastRevisionText() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $row = $dbr->selectRow( 'archive',
-                       array( 'ar_text', 'ar_flags', 'ar_text_id' ),
-                       array( 'ar_namespace' => $this->title->getNamespace(),
-                              'ar_title' => $this->title->getDBkey() ),
-                       'PageArchive::getLastRevisionText',
-                       array( 'ORDER BY' => 'ar_timestamp DESC' ) );
-               if( $row ) {
-                       return $this->getTextFromRow( $row );
-               } else {
-                       return NULL;
-               }
-       }
-
-       /**
-        * Quick check if any archived revisions are present for the page.
-        * @return bool
-        */
-       function isDeleted() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
-                       array( 'ar_namespace' => $this->title->getNamespace(),
-                              'ar_title' => $this->title->getDBkey() ) );
-               return ($n > 0);
-       }
-
-       /**
-        * Restore the given (or all) text and file revisions for the page.
-        * Once restored, the items will be removed from the archive tables.
-        * The deletion log will be updated with an undeletion notice.
-        *
-        * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
-        * @param string $comment
-        * @param array $fileVersions
-        * @param bool $unsuppress
-        *
-        * @return array(number of file revisions restored, number of image revisions restored, log message)
-        * on success, false on failure
-        */
-       function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) {
-               // If both the set of text revisions and file revisions are empty,
-               // restore everything. Otherwise, just restore the requested items.
-               $restoreAll = empty( $timestamps ) && empty( $fileVersions );
-
-               $restoreText = $restoreAll || !empty( $timestamps );
-               $restoreFiles = $restoreAll || !empty( $fileVersions );
-
-               if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
-                       $img = wfLocalFile( $this->title );
-                       $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
-                       $filesRestored = $this->fileStatus->successCount;
-               } else {
-                       $filesRestored = 0;
-               }
-
-               if( $restoreText ) {
-                       $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress );
-                       if($textRestored === false) // It must be one of UNDELETE_*
-                               return false;
-               } else {
-                       $textRestored = 0;
-               }
-
-               // Touch the log!
-               global $wgContLang;
-               $log = new LogPage( 'delete' );
-
-               if( $textRestored && $filesRestored ) {
-                       $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
-                               $wgContLang->formatNum( $textRestored ),
-                               $wgContLang->formatNum( $filesRestored ) );
-               } elseif( $textRestored ) {
-                       $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
-                               $wgContLang->formatNum( $textRestored ) );
-               } elseif( $filesRestored ) {
-                       $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
-                               $wgContLang->formatNum( $filesRestored ) );
-               } else {
-                       wfDebug( "Undelete: nothing undeleted...\n" );
-                       return false;
-               }
-
-               if( trim( $comment ) != '' )
-                       $reason .= ": {$comment}";
-               $log->addEntry( 'restore', $this->title, $reason );
-
-               return array($textRestored, $filesRestored, $reason);
-       }
-
-       /**
-        * This is the meaty bit -- restores archived revisions of the given page
-        * to the cur/old tables. If the page currently exists, all revisions will
-        * be stuffed into old, otherwise the most recent will go into cur.
-        *
-        * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
-        * @param string $comment
-        * @param array $fileVersions
-        * @param bool $unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
-        *
-        * @return mixed number of revisions restored or false on failure
-        */
-       private function undeleteRevisions( $timestamps, $unsuppress = false ) {
-               if ( wfReadOnly() )
-                       return false;
-               $restoreAll = empty( $timestamps );
-
-               $dbw = wfGetDB( DB_MASTER );
-
-               # Does this page already exist? We'll have to update it...
-               $article = new Article( $this->title );
-               $options = 'FOR UPDATE';
-               $page = $dbw->selectRow( 'page',
-                       array( 'page_id', 'page_latest' ),
-                       array( 'page_namespace' => $this->title->getNamespace(),
-                              'page_title'     => $this->title->getDBkey() ),
-                       __METHOD__,
-                       $options );
-               if( $page ) {
-                       $makepage = false;
-                       # Page already exists. Import the history, and if necessary
-                       # we'll update the latest revision field in the record.
-                       $newid             = 0;
-                       $pageId            = $page->page_id;
-                       $previousRevId     = $page->page_latest;
-                       # Get the time span of this page
-                       $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
-                               array( 'rev_id' => $previousRevId ),
-                               __METHOD__ );
-                       if( $previousTimestamp === false ) {
-                               wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
-                               return 0;
-                       }
-               } else {
-                       # Have to create a new article...
-                       $makepage = true;
-                       $previousRevId = 0;
-                       $previousTimestamp = 0;
-               }
-
-               if( $restoreAll ) {
-                       $oldones = '1 = 1'; # All revisions...
-               } else {
-                       $oldts = implode( ',',
-                               array_map( array( &$dbw, 'addQuotes' ),
-                                       array_map( array( &$dbw, 'timestamp' ),
-                                               $timestamps ) ) );
-
-                       $oldones = "ar_timestamp IN ( {$oldts} )";
-               }
-
-               /**
-                * Select each archived revision...
-                */
-               $result = $dbw->select( 'archive',
-                       /* fields */ array(
-                               'ar_rev_id',
-                               'ar_text',
-                               'ar_comment',
-                               'ar_user',
-                               'ar_user_text',
-                               'ar_timestamp',
-                               'ar_minor_edit',
-                               'ar_flags',
-                               'ar_text_id',
-                               'ar_deleted',
-                               'ar_page_id',
-                               'ar_len' ),
-                       /* WHERE */ array(
-                               'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title'     => $this->title->getDBkey(),
-                               $oldones ),
-                       __METHOD__,
-                       /* options */ array(
-                               'ORDER BY' => 'ar_timestamp' )
-                       );
-               $ret = $dbw->resultObject( $result );
-
-               $rev_count = $dbw->numRows( $result );
-               if( $rev_count ) {
-                       # We need to seek around as just using DESC in the ORDER BY
-                       # would leave the revisions inserted in the wrong order
-                       $first = $ret->fetchObject();
-                       $ret->seek( $rev_count - 1 );
-                       $last = $ret->fetchObject();
-                       // We don't handle well changing the top revision's settings
-                       if( !$unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
-                               wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
-                               return false;
-                       }
-                       $ret->seek( 0 );
-               }
-
-               if( $makepage ) {
-                       $newid  = $article->insertOn( $dbw );
-                       $pageId = $newid;
-               }
-
-               $revision = null;
-               $restored = 0;
-
-               while( $row = $ret->fetchObject() ) {
-                       if( $row->ar_text_id ) {
-                               // Revision was deleted in 1.5+; text is in
-                               // the regular text table, use the reference.
-                               // Specify null here so the so the text is
-                               // dereferenced for page length info if needed.
-                               $revText = null;
-                       } else {
-                               // Revision was deleted in 1.4 or earlier.
-                               // Text is squashed into the archive row, and
-                               // a new text table entry will be created for it.
-                               $revText = Revision::getRevisionText( $row, 'ar_' );
-                       }
-                       $revision = new Revision( array(
-                               'page'       => $pageId,
-                               'id'         => $row->ar_rev_id,
-                               'text'       => $revText,
-                               'comment'    => $row->ar_comment,
-                               'user'       => $row->ar_user,
-                               'user_text'  => $row->ar_user_text,
-                               'timestamp'  => $row->ar_timestamp,
-                               'minor_edit' => $row->ar_minor_edit,
-                               'text_id'    => $row->ar_text_id,
-                               'deleted'        => $unsuppress ? 0 : $row->ar_deleted,
-                               'len'        => $row->ar_len
-                               ) );
-                       $revision->insertOn( $dbw );
-                       $restored++;
-
-                       wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
-               }
-               // Was anything restored at all?
-               if($restored == 0)
-                       return 0;
-
-               if( $revision ) {
-                       // Attach the latest revision to the page...
-                       $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
-
-                       if( $newid || $wasnew ) {
-                               // Update site stats, link tables, etc
-                               $article->createUpdates( $revision );
-                       }
-
-                       if( $newid ) {
-                               wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
-                               Article::onArticleCreate( $this->title );
-                       } else {
-                               wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
-                               Article::onArticleEdit( $this->title );
-                       }
-
-                       if( $this->title->getNamespace() == NS_IMAGE ) {
-                               $update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
-                               $update->doUpdate();
-                       }
-               } else {
-                       // Revision couldn't be created. This is very weird
-                       return self::UNDELETE_UNKNOWNERR;
-               }
-
-               # Now that it's safely stored, take it out of the archive
-               $dbw->delete( 'archive',
-                       /* WHERE */ array(
-                               'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey(),
-                               $oldones ),
-                       __METHOD__ );
-
-               return $restored;
-       }
-
-       function getFileStatus() { return $this->fileStatus; }
-}
-
-/**
- * The HTML form for Special:Undelete, which allows users with the appropriate
- * permissions to view and restore deleted content.
- * @ingroup SpecialPage
- */
-class UndeleteForm {
-       var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
-       var $mTargetTimestamp, $mAllowed, $mComment;
-
-       function UndeleteForm( $request, $par = "" ) {
-               global $wgUser;
-               $this->mAction = $request->getVal( 'action' );
-               $this->mTarget = $request->getVal( 'target' );
-               $this->mSearchPrefix = $request->getText( 'prefix' );
-               $time = $request->getVal( 'timestamp' );
-               $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
-               $this->mFile = $request->getVal( 'file' );
-
-               $posted = $request->wasPosted() &&
-                       $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
-               $this->mRestore = $request->getCheck( 'restore' ) && $posted;
-               $this->mPreview = $request->getCheck( 'preview' ) && $posted;
-               $this->mDiff = $request->getCheck( 'diff' );
-               $this->mComment = $request->getText( 'wpComment' );
-               $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
-
-               if( $par != "" ) {
-                       $this->mTarget = $par;
-               }
-               if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
-                       $this->mAllowed = true;
-               } else {
-                       $this->mAllowed = false;
-                       $this->mTimestamp = '';
-                       $this->mRestore = false;
-               }
-               if ( $this->mTarget !== "" ) {
-                       $this->mTargetObj = Title::newFromURL( $this->mTarget );
-               } else {
-                       $this->mTargetObj = NULL;
-               }
-               if( $this->mRestore ) {
-                       $timestamps = array();
-                       $this->mFileVersions = array();
-                       foreach( $_REQUEST as $key => $val ) {
-                               $matches = array();
-                               if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
-                                       array_push( $timestamps, $matches[1] );
-                               }
-
-                               if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
-                                       $this->mFileVersions[] = intval( $matches[1] );
-                               }
-                       }
-                       rsort( $timestamps );
-                       $this->mTargetTimestamp = $timestamps;
-               }
-       }
-
-       function execute() {
-               global $wgOut, $wgUser;
-               if ( $this->mAllowed ) {
-                       $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
-               } else {
-                       $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
-               }
-
-               if( is_null( $this->mTargetObj ) ) {
-               # Not all users can just browse every deleted page from the list
-                       if( $wgUser->isAllowed( 'browsearchive' ) ) {
-                               $this->showSearchForm();
-
-                               # List undeletable articles
-                               if( $this->mSearchPrefix ) {
-                                       $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
-                                       $this->showList( $result );
-                               }
-                       } else {
-                               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
-                       }
-                       return;
-               }
-               if( $this->mTimestamp !== '' ) {
-                       return $this->showRevision( $this->mTimestamp );
-               }
-               if( $this->mFile !== null ) {
-                       $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
-                       // Check if user is allowed to see this file
-                       if( !$file->userCan( File::DELETED_FILE ) ) {
-                               $wgOut->permissionRequired( 'suppressrevision' );
-                               return false;
-                       } else {
-                               return $this->showFile( $this->mFile );
-                       }
-               }
-               if( $this->mRestore && $this->mAction == "submit" ) {
-                       return $this->undelete();
-               }
-               return $this->showHistory();
-       }
-
-       function showSearchForm() {
-               global $wgOut, $wgScript;
-               $wgOut->addWikiMsg( 'undelete-header' );
-
-               $wgOut->addHtml(
-                       Xml::openElement( 'form', array(
-                               'method' => 'get',
-                               'action' => $wgScript ) ) .
-                       '<fieldset>' .
-                       Xml::element( 'legend', array(),
-                               wfMsg( 'undelete-search-box' ) ) .
-                       Xml::hidden( 'title',
-                               SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
-                       Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
-                               'prefix', 'prefix', 20,
-                               $this->mSearchPrefix ) .
-                       Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
-                       '</fieldset>' .
-                       '</form>' );
-       }
-
-       // Generic list of deleted pages
-       private function showList( $result ) {
-               global $wgLang, $wgContLang, $wgUser, $wgOut;
-
-               if( $result->numRows() == 0 ) {
-                       $wgOut->addWikiMsg( 'undelete-no-results' );
-                       return;
-               }
-
-               $wgOut->addWikiMsg( "undeletepagetext" );
-
-               $sk = $wgUser->getSkin();
-               $undelete = SpecialPage::getTitleFor( 'Undelete' );
-               $wgOut->addHTML( "<ul>\n" );
-               while( $row = $result->fetchObject() ) {
-                       $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
-                       $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ),
-                               'target=' . $title->getPrefixedUrl() );
-                       #$revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) );
-                       $revs = wfMsgExt( 'undeleterevisions',
-                               array( 'parseinline' ),
-                               $wgLang->formatNum( $row->count ) );
-                       $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" );
-               }
-               $result->free();
-               $wgOut->addHTML( "</ul>\n" );
-
-               return true;
-       }
-
-       private function showRevision( $timestamp ) {
-               global $wgLang, $wgUser, $wgOut;
-               $self = SpecialPage::getTitleFor( 'Undelete' );
-               $skin = $wgUser->getSkin();
-
-               if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
-
-               $archive = new PageArchive( $this->mTargetObj );
-               $rev = $archive->getRevision( $timestamp );
-
-               if( !$rev ) {
-                       $wgOut->addWikiMsg( 'undeleterevision-missing' );
-                       return;
-               }
-
-               if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
-                       if( !$rev->userCan(Revision::DELETED_TEXT) ) {
-                               $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
-                               return;
-                       } else {
-                               $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
-                               $wgOut->addHTML( '<br/>' );
-                               // and we are allowed to see...
-                       }
-               }
-
-               $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
-
-               $link = $skin->makeKnownLinkObj(
-                       SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
-                       htmlspecialchars( $this->mTargetObj->getPrefixedText() )
-               );
-               $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
-               $user = $skin->revUserTools( $rev );
-
-               if( $this->mDiff ) {
-                       $previousRev = $archive->getPreviousRevision( $timestamp );
-                       if( $previousRev ) {
-                               $this->showDiff( $previousRev, $rev );
-                               if( $wgUser->getOption( 'diffonly' ) ) {
-                                       return;
-                               } else {
-                                       $wgOut->addHtml( '<hr />' );
-                               }
-                       } else {
-                               $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) );
-                       }
-               }
-
-               $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
-
-               wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
-
-               if( $this->mPreview ) {
-                       $wgOut->addHtml( "<hr />\n" );
-
-                       //Hide [edit]s
-                       $popts = $wgOut->parserOptions();
-                       $popts->setEditSection( false );
-                       $wgOut->parserOptions( $popts );
-                       $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, true );
-               }
-
-               $wgOut->addHtml(
-                       wfElement( 'textarea', array(
-                                       'readonly' => 'readonly',
-                                       'cols' => intval( $wgUser->getOption( 'cols' ) ),
-                                       'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
-                               $rev->revText() . "\n" ) .
-                       wfOpenElement( 'div' ) .
-                       wfOpenElement( 'form', array(
-                               'method' => 'post',
-                               'action' => $self->getLocalURL( "action=submit" ) ) ) .
-                       wfElement( 'input', array(
-                               'type' => 'hidden',
-                               'name' => 'target',
-                               'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
-                       wfElement( 'input', array(
-                               'type' => 'hidden',
-                               'name' => 'timestamp',
-                               'value' => $timestamp ) ) .
-                       wfElement( 'input', array(
-                               'type' => 'hidden',
-                               'name' => 'wpEditToken',
-                               'value' => $wgUser->editToken() ) ) .
-                       wfElement( 'input', array(
-                               'type' => 'submit',
-                               'name' => 'preview',
-                               'value' => wfMsg( 'showpreview' ) ) ) .
-                       wfElement( 'input', array(
-                               'name' => 'diff',
-                               'type' => 'submit',
-                               'value' => wfMsg( 'showdiff' ) ) ) .
-                       wfCloseElement( 'form' ) .
-                       wfCloseElement( 'div' ) );
-       }
-
-       /**
-        * Build a diff display between this and the previous either deleted
-        * or non-deleted edit.
-        * @param Revision $previousRev
-        * @param Revision $currentRev
-        * @return string HTML
-        */
-       function showDiff( $previousRev, $currentRev ) {
-               global $wgOut, $wgUser;
-
-               $diffEngine = new DifferenceEngine();
-               $diffEngine->showDiffStyle();
-               $wgOut->addHtml(
-                       "<div>" .
-                       "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
-                       "<col class='diff-marker' />" .
-                       "<col class='diff-content' />" .
-                       "<col class='diff-marker' />" .
-                       "<col class='diff-content' />" .
-                       "<tr>" .
-                               "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
-                               $this->diffHeader( $previousRev ) .
-                               "</td>" .
-                               "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
-                               $this->diffHeader( $currentRev ) .
-                               "</td>" .
-                       "</tr>" .
-                       $diffEngine->generateDiffBody(
-                               $previousRev->getText(), $currentRev->getText() ) .
-                       "</table>" .
-                       "</div>\n" );
-
-       }
-
-       private function diffHeader( $rev ) {
-               global $wgUser, $wgLang, $wgLang;
-               $sk = $wgUser->getSkin();
-               $isDeleted = !( $rev->getId() && $rev->getTitle() );
-               if( $isDeleted ) {
-                       /// @fixme $rev->getTitle() is null for deleted revs...?
-                       $targetPage = SpecialPage::getTitleFor( 'Undelete' );
-                       $targetQuery = 'target=' .
-                               $this->mTargetObj->getPrefixedUrl() .
-                               '&timestamp=' .
-                               wfTimestamp( TS_MW, $rev->getTimestamp() );
-               } else {
-                       /// @fixme getId() may return non-zero for deleted revs...
-                       $targetPage = $rev->getTitle();
-                       $targetQuery = 'oldid=' . $rev->getId();
-               }
-               return
-                       '<div id="mw-diff-otitle1"><strong>' .
-                               $sk->makeLinkObj( $targetPage,
-                                       wfMsgHtml( 'revisionasof',
-                                               $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
-                                       $targetQuery ) .
-                               ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
-                       '</strong></div>' .
-                       '<div id="mw-diff-otitle2">' .
-                               $sk->revUserTools( $rev ) . '<br/>' .
-                       '</div>' .
-                       '<div id="mw-diff-otitle3">' .
-                               $sk->revComment( $rev ) . '<br/>' .
-                       '</div>';
-       }
-
-       /**
-        * Show a deleted file version requested by the visitor.
-        */
-       private function showFile( $key ) {
-               global $wgOut, $wgRequest;
-               $wgOut->disable();
-
-               # We mustn't allow the output to be Squid cached, otherwise
-               # if an admin previews a deleted image, and it's cached, then
-               # a user without appropriate permissions can toddle off and
-               # nab the image, and Squid will serve it
-               $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
-               $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
-               $wgRequest->response()->header( 'Pragma: no-cache' );
-
-               $store = FileStore::get( 'deleted' );
-               $store->stream( $key );
-       }
-
-       private function showHistory() {
-               global $wgLang, $wgUser, $wgOut;
-
-               $sk = $wgUser->getSkin();
-               if( $this->mAllowed ) {
-                       $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
-               } else {
-                       $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
-               }
-
-               $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
-
-               $archive = new PageArchive( $this->mTargetObj );
-               /*
-               $text = $archive->getLastRevisionText();
-               if( is_null( $text ) ) {
-                       $wgOut->addWikiMsg( "nohistory" );
-                       return;
-               }
-               */
-               if ( $this->mAllowed ) {
-                       $wgOut->addWikiMsg( "undeletehistory" );
-                       $wgOut->addWikiMsg( "undeleterevdel" );
-               } else {
-                       $wgOut->addWikiMsg( "undeletehistorynoadmin" );
-               }
-
-               # List all stored revisions
-               $revisions = $archive->listRevisions();
-               $files = $archive->listFiles();
-
-               $haveRevisions = $revisions && $revisions->numRows() > 0;
-               $haveFiles = $files && $files->numRows() > 0;
-
-               # Batch existence check on user and talk pages
-               if( $haveRevisions ) {
-                       $batch = new LinkBatch();
-                       while( $row = $revisions->fetchObject() ) {
-                               $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
-                               $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
-                       }
-                       $batch->execute();
-                       $revisions->seek( 0 );
-               }
-               if( $haveFiles ) {
-                       $batch = new LinkBatch();
-                       while( $row = $files->fetchObject() ) {
-                               $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
-                               $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
-                       }
-                       $batch->execute();
-                       $files->seek( 0 );
-               }
-
-               if ( $this->mAllowed ) {
-                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
-                       $action = $titleObj->getLocalURL( "action=submit" );
-                       # Start the form here
-                       $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
-                       $wgOut->addHtml( $top );
-               }
-
-               # Show relevant lines from the deletion log:
-               $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" );
-               LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTargetObj->getPrefixedText() );
-
-               if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
-                       # Format the user-visible controls (comment field, submission button)
-                       # in a nice little table
-                       if( $wgUser->isAllowed( 'suppressrevision' ) ) {
-                               $unsuppressBox =
-                                       "<tr>
-                                               <td>&nbsp;</td>
-                                               <td class='mw-input'>" .
-                                                       Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress',
-                                                               'mw-undelete-unsuppress', $this->mUnsuppress ).
-                                               "</td>
-                                       </tr>";
-                       } else {
-                               $unsuppressBox = "";
-                       }
-                       $table =
-                               Xml::openElement( 'fieldset' ) .
-                               Xml::element( 'legend', null, wfMsg( 'undelete') ).
-                               Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
-                                       "<tr>
-                                               <td colspan='2'>" .
-                                                       wfMsgWikiHtml( 'undeleteextrahelp' ) .
-                                               "</td>
-                                       </tr>
-                                       <tr>
-                                               <td class='mw-label'>" .
-                                                       Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
-                                               "</td>
-                                               <td class='mw-input'>" .
-                                                       Xml::input( 'wpComment', 50, $this->mComment, array( 'id' =>  'wpComment' ) ) .
-                                               "</td>
-                                       </tr>
-                                       <tr>
-                                               <td>&nbsp;</td>
-                                               <td class='mw-submit'>" .
-                                                       Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
-                                                       Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
-                                               "</td>
-                                       </tr>" .
-                                       $unsuppressBox .
-                               Xml::closeElement( 'table' ) .
-                               Xml::closeElement( 'fieldset' );
-
-                       $wgOut->addHtml( $table );
-               }
-
-               $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" );
-
-               if( $haveRevisions ) {
-                       # The page's stored (deleted) history:
-                       $wgOut->addHTML("<ul>");
-                       $target = urlencode( $this->mTarget );
-                       $remaining = $revisions->numRows();
-                       $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj );
-
-                       while( $row = $revisions->fetchObject() ) {
-                               $remaining--;
-                               $wgOut->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) );
-                       }
-                       $revisions->free();
-                       $wgOut->addHTML("</ul>");
-               } else {
-                       $wgOut->addWikiMsg( "nohistory" );
-               }
-
-               if( $haveFiles ) {
-                       $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
-                       $wgOut->addHtml( "<ul>" );
-                       while( $row = $files->fetchObject() ) {
-                               $wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
-                       }
-                       $files->free();
-                       $wgOut->addHTML( "</ul>" );
-               }
-
-               if ( $this->mAllowed ) {
-                       # Slip in the hidden controls here
-                       $misc  = Xml::hidden( 'target', $this->mTarget );
-                       $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
-                       $misc .= Xml::closeElement( 'form' );
-                       $wgOut->addHtml( $misc );
-               }
-
-               return true;
-       }
-
-       private function formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) {
-               global $wgUser, $wgLang;
-
-               $rev = new Revision( array(
-                               'page'       => $this->mTargetObj->getArticleId(),
-                               'comment'    => $row->ar_comment,
-                               'user'       => $row->ar_user,
-                               'user_text'  => $row->ar_user_text,
-                               'timestamp'  => $row->ar_timestamp,
-                               'minor_edit' => $row->ar_minor_edit,
-                               'deleted'    => $row->ar_deleted,
-                               'len'        => $row->ar_len ) );
-
-               $stxt = '';
-               $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
-               if( $this->mAllowed ) {
-                       $checkBox = Xml::check( "ts$ts" );
-                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
-                       $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
-                       # Last link
-                       if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
-                               $last = wfMsgHtml('diff');
-                       } else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) {
-                               $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'),
-                                       "target=" . $this->mTargetObj->getPrefixedUrl() . "&timestamp=$ts&diff=prev" );
-                       } else {
-                               $last = wfMsgHtml('diff');
-                       }
-               } else {
-                       $checkBox = '';
-                       $pageLink = $wgLang->timeanddate( $ts, true );
-                       $last = wfMsgHtml('diff');
-               }
-               $userLink = $sk->revUserTools( $rev );
-
-               if(!is_null($size = $row->ar_len)) {
-                       if($size == 0)
-                               $stxt = wfMsgHtml('historyempty');
-                       else
-                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
-               }
-               $comment = $sk->revComment( $rev );
-               $revdlink = '';
-               if( $wgUser->isAllowed( 'deleterevision' ) ) {
-                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
-                       if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
-                       // If revision was hidden from sysops
-                               $del = wfMsgHtml('rev-delundel');
-                       } else {
-                               $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
-                               $del = $sk->makeKnownLinkObj( $revdel,
-                                       wfMsgHtml('rev-delundel'),
-                                       'target=' . $this->mTargetObj->getPrefixedUrl() . "&artimestamp=$ts" );
-                               // Bolden oversighted content
-                               if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
-                                       $del = "<strong>$del</strong>";
-                       }
-                       $revdlink = "<tt>(<small>$del</small>)</tt>";
-               }
-
-               return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
-       }
-
-       private function formatFileRow( $row, $sk ) {
-               global $wgUser, $wgLang;
-
-               $file = ArchivedFile::newFromRow( $row );
-
-               $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
-               if( $this->mAllowed && $row->fa_storage_key ) {
-                       $checkBox = Xml::check( "fileid" . $row->fa_id );
-                       $key = urlencode( $row->fa_storage_key );
-                       $target = urlencode( $this->mTarget );
-                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
-                       $pageLink = $this->getFileLink( $file, $titleObj, $ts, $key, $sk );
-               } else {
-                       $checkBox = '';
-                       $pageLink = $wgLang->timeanddate( $ts, true );
-               }
-               $userLink = $this->getFileUser( $file, $sk );
-               $data =
-                       wfMsgHtml( 'widthheight',
-                               $wgLang->formatNum( $row->fa_width ),
-                               $wgLang->formatNum( $row->fa_height ) ) .
-                       ' (' .
-                       wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
-                       ')';
-               $comment = $this->getFileComment( $file, $sk );
-               $revdlink = '';
-               if( $wgUser->isAllowed( 'deleterevision' ) ) {
-                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
-                       if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
-                       // If revision was hidden from sysops
-                               $del = wfMsgHtml('rev-delundel');
-                       } else {
-                               $del = $sk->makeKnownLinkObj( $revdel,
-                                       wfMsgHtml('rev-delundel'),
-                                       'target=' . $this->mTargetObj->getPrefixedUrl() .
-                                       '&fileid=' . $row->fa_id );
-                               // Bolden oversighted content
-                               if( $file->isDeleted( File::DELETED_RESTRICTED ) )
-                                       $del = "<strong>$del</strong>";
-                       }
-                       $revdlink = "<tt>(<small>$del</small>)</tt>";
-               }
-               return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
-       }
-
-       private function getEarliestTime( $title ) {
-               $dbr = wfGetDB( DB_SLAVE );
-               if( $title->exists() ) {
-                       $min = $dbr->selectField( 'revision',
-                               'MIN(rev_timestamp)',
-                               array( 'rev_page' => $title->getArticleId() ),
-                               __METHOD__ );
-                       return wfTimestampOrNull( TS_MW, $min );
-               }
-               return null;
-       }
-
-       /**
-        * Fetch revision text link if it's available to all users
-        * @return string
-        */
-       function getPageLink( $rev, $titleObj, $ts, $sk ) {
-               global $wgLang;
-
-               if( !$rev->userCan(Revision::DELETED_TEXT) ) {
-                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
-               } else {
-                       $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
-                               "target=".$this->mTargetObj->getPrefixedUrl()."&timestamp=$ts" );
-                       if( $rev->isDeleted(Revision::DELETED_TEXT) )
-                               $link = '<span class="history-deleted">' . $link . '</span>';
-                       return $link;
-               }
-       }
-
-       /**
-        * Fetch image view link if it's available to all users
-        * @return string
-        */
-       function getFileLink( $file, $titleObj, $ts, $key, $sk ) {
-               global $wgLang;
-
-               if( !$file->userCan(File::DELETED_FILE) ) {
-                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
-               } else {
-                       $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
-                               "target=".$this->mTargetObj->getPrefixedUrl()."&file=$key" );
-                       if( $file->isDeleted(File::DELETED_FILE) )
-                               $link = '<span class="history-deleted">' . $link . '</span>';
-                       return $link;
-               }
-       }
-
-       /**
-        * Fetch file's user id if it's available to this user
-        * @return string
-        */
-       function getFileUser( $file, $sk ) {
-               if( !$file->userCan(File::DELETED_USER) ) {
-                       return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
-               } else {
-                       $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) .
-                               $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() );
-                       if( $file->isDeleted(File::DELETED_USER) )
-                               $link = '<span class="history-deleted">' . $link . '</span>';
-                       return $link;
-               }
-       }
-
-       /**
-        * Fetch file upload comment if it's available to this user
-        * @return string
-        */
-       function getFileComment( $file, $sk ) {
-               if( !$file->userCan(File::DELETED_COMMENT) ) {
-                       return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
-               } else {
-                       $link = $sk->commentBlock( $file->getRawDescription() );
-                       if( $file->isDeleted(File::DELETED_COMMENT) )
-                               $link = '<span class="history-deleted">' . $link . '</span>';
-                       return $link;
-               }
-       }
-
-       function undelete() {
-               global $wgOut, $wgUser;
-               if ( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-               if( !is_null( $this->mTargetObj ) ) {
-                       $archive = new PageArchive( $this->mTargetObj );
-                       $ok = $archive->undelete(
-                               $this->mTargetTimestamp,
-                               $this->mComment,
-                               $this->mFileVersions,
-                               $this->mUnsuppress );
-
-                       if( is_array($ok) ) {
-                               if ( $ok[1] ) // Undeleted file count
-                                       wfRunHooks( 'FileUndeleteComplete', array(
-                                               $this->mTargetObj, $this->mFileVersions,
-                                               $wgUser, $this->mComment) );
-
-                               $skin = $wgUser->getSkin();
-                               $link = $skin->makeKnownLinkObj( $this->mTargetObj );
-                               $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
-                       } else {
-                               $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
-                               $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
-                       }
-
-                       // Show file deletion warnings and errors
-                       $status = $archive->getFileStatus();
-                       if( $status && !$status->isGood() ) {
-                               $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
-                       }
-               } else {
-                       $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
-               }
-               return false;
-       }
-}
diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php
deleted file mode 100644 (file)
index 0bf7e5a..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- *
- */
-function wfSpecialUnlockdb() {
-       global $wgUser, $wgOut, $wgRequest;
-
-       if( !$wgUser->isAllowed( 'siteadmin' ) ) {
-               $wgOut->permissionRequired( 'siteadmin' );
-               return;
-       }
-
-       $action = $wgRequest->getVal( 'action' );
-       $f = new DBUnlockForm();
-
-       if ( "success" == $action ) {
-               $f->showSuccess();
-       } else if ( "submit" == $action && $wgRequest->wasPosted() &&
-               $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
-               $f->doSubmit();
-       } else {
-               $f->showForm( "" );
-       }
-}
-
-/**
- * @ingroup SpecialPage
- */
-class DBUnlockForm {
-       function showForm( $err )
-       {
-               global $wgOut, $wgUser;
-
-               global $wgReadOnlyFile;
-               if( !file_exists( $wgReadOnlyFile ) ) {
-                       $wgOut->addWikiMsg( 'databasenotlocked' );
-                       return;
-               }
-
-               $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
-               $wgOut->addWikiMsg( "unlockdbtext" );
-
-               if ( "" != $err ) {
-                       $wgOut->setSubtitle( wfMsg( "formerror" ) );
-                       $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
-               }
-               $lc = htmlspecialchars( wfMsg( "unlockconfirm" ) );
-               $lb = htmlspecialchars( wfMsg( "unlockbtn" ) );
-               $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
-               $action = $titleObj->escapeLocalURL( "action=submit" );
-               $token = htmlspecialchars( $wgUser->editToken() );
-
-               $wgOut->addHTML( <<<END
-
-<form id="unlockdb" method="post" action="{$action}">
-<table border="0">
-       <tr>
-               <td align="right">
-                       <input type="checkbox" name="wpLockConfirm" />
-               </td>
-               <td align="left">{$lc}</td>
-       </tr>
-       <tr>
-               <td>&nbsp;</td>
-               <td align="left">
-                       <input type="submit" name="wpLock" value="{$lb}" />
-               </td>
-       </tr>
-</table>
-<input type="hidden" name="wpEditToken" value="{$token}" />
-</form>
-END
-);
-
-       }
-
-       function doSubmit() {
-               global $wgOut, $wgRequest, $wgReadOnlyFile;
-
-               $wpLockConfirm = $wgRequest->getCheck( 'wpLockConfirm' );
-               if ( ! $wpLockConfirm ) {
-                       $this->showForm( wfMsg( "locknoconfirm" ) );
-                       return;
-               }
-               if ( @! unlink( $wgReadOnlyFile ) ) {
-                       $wgOut->showFileDeleteError( $wgReadOnlyFile );
-                       return;
-               }
-               $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
-               $success = $titleObj->getFullURL( "action=success" );
-               $wgOut->redirect( $success );
-       }
-
-       function showSuccess() {
-               global $wgOut;
-               global $ip;
-
-               $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
-               $wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) );
-               $wgOut->addWikiMsg( "unlockdbsuccesstext", $ip );
-       }
-}
diff --git a/includes/SpecialUnusedcategories.php b/includes/SpecialUnusedcategories.php
deleted file mode 100644 (file)
index 406f794..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * @ingroup SpecialPage
- */
-class UnusedCategoriesPage extends QueryPage {
-
-       function isExpensive() { return true; }
-
-       function getName() {
-               return 'Unusedcategories';
-       }
-
-       function getPageHeader() {
-               return wfMsgExt( 'unusedcategoriestext', array( 'parse' ) );
-       }
-
-       function getSQL() {
-               $NScat = NS_CATEGORY;
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
-               return "SELECT 'Unusedcategories' as type,
-                               {$NScat} as namespace, page_title as title, page_title as value
-                               FROM $page
-                               LEFT JOIN $categorylinks ON page_title=cl_to
-                               WHERE cl_from IS NULL
-                               AND page_namespace = {$NScat}
-                               AND page_is_redirect = 0";
-       }
-
-       function formatResult( $skin, $result ) {
-               $title = Title::makeTitle( NS_CATEGORY, $result->title );
-               return $skin->makeLinkObj( $title, $title->getText() );
-       }
-}
-
-/** constructor */
-function wfSpecialUnusedCategories() {
-       list( $limit, $offset ) = wfCheckLimits();
-       $uc = new UnusedCategoriesPage();
-       return $uc->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php
deleted file mode 100644 (file)
index d71b638..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * implements Special:Unusedimages
- * @ingroup SpecialPage
- */
-class UnusedimagesPage extends ImageQueryPage {
-
-       function isExpensive() { return true; }
-
-       function getName() {
-               return 'Unusedimages';
-       }
-
-       function sortDescending() {
-               return false;
-       }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               global $wgCountCategorizedImagesAsUsed;
-               $dbr = wfGetDB( DB_SLAVE );
-
-               if ( $wgCountCategorizedImagesAsUsed ) {
-                       list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' );
-
-                       return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
-                                               img_user, img_user_text,  img_description
-                                       FROM ((($page AS I LEFT JOIN $categorylinks AS L ON I.page_id = L.cl_from)
-                                               LEFT JOIN $imagelinks AS P ON I.page_title = P.il_to)
-                                               INNER JOIN $image AS G ON I.page_title = G.img_name)
-                                       WHERE I.page_namespace = ".NS_IMAGE." AND L.cl_from IS NULL AND P.il_to IS NULL";
-               } else {
-                       list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' );
-
-                       return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
-                               img_user, img_user_text,  img_description
-                               FROM $image LEFT JOIN $imagelinks ON img_name=il_to WHERE il_to IS NULL ";
-               }
-       }
-
-       function getPageHeader() {
-               return wfMsgExt( 'unusedimagestext', array( 'parse') );
-       }
-
-}
-
-/**
- * Entry point
- */
-function wfSpecialUnusedimages() {
-       list( $limit, $offset ) = wfCheckLimits();
-       $uip = new UnusedimagesPage();
-
-       return $uip->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUnusedtemplates.php b/includes/SpecialUnusedtemplates.php
deleted file mode 100644 (file)
index 89acd09..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * implements Special:Unusedtemplates
- * @author Rob Church <robchur@gmail.com>
- * @copyright Â© 2006 Rob Church
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- * @ingroup SpecialPage
- */
-class UnusedtemplatesPage extends QueryPage {
-
-       function getName() { return( 'Unusedtemplates' ); }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-       function sortDescending() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $templatelinks) = $dbr->tableNamesN( 'page', 'templatelinks' );
-               $sql = "SELECT 'Unusedtemplates' AS type, page_title AS title,
-                       page_namespace AS namespace, 0 AS value
-                       FROM $page
-                       LEFT JOIN $templatelinks
-                       ON page_namespace = tl_namespace AND page_title = tl_title
-                       WHERE page_namespace = 10 AND tl_from IS NULL
-                       AND page_is_redirect = 0";
-               return $sql;
-       }
-
-       function formatResult( $skin, $result ) {
-               $title = Title::makeTitle( NS_TEMPLATE, $result->title );
-               $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' );
-               $wlhLink = $skin->makeKnownLinkObj(
-                       SpecialPage::getTitleFor( 'Whatlinkshere' ),
-                       wfMsgHtml( 'unusedtemplateswlh' ),
-                       'target=' . $title->getPrefixedUrl() );
-               return wfSpecialList( $pageLink, $wlhLink );
-       }
-
-       function getPageHeader() {
-               return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) );
-       }
-
-}
-
-function wfSpecialUnusedtemplates() {
-       list( $limit, $offset ) = wfCheckLimits();
-       $utp = new UnusedtemplatesPage();
-       $utp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUnwatchedpages.php b/includes/SpecialUnwatchedpages.php
deleted file mode 100644 (file)
index 64ab372..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A special page that displays a list of pages that are not on anyones watchlist.
- * Implements Special:Unwatchedpages
- *
- * @ingroup SpecialPage
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-class UnwatchedpagesPage extends QueryPage {
-
-       function getName() { return 'Unwatchedpages'; }
-       function isExpensive() { return true; }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $watchlist ) = $dbr->tableNamesN( 'page', 'watchlist' );
-               $mwns = NS_MEDIAWIKI;
-               return
-                       "
-                       SELECT
-                               'Unwatchedpages' as type,
-                               page_namespace as namespace,
-                               page_title as title,
-                               page_namespace as value
-                       FROM $page
-                       LEFT JOIN $watchlist ON wl_namespace = page_namespace AND page_title = wl_title
-                       WHERE wl_title IS NULL AND page_is_redirect = 0 AND page_namespace<>$mwns
-                       ";
-       }
-
-       function sortDescending() { return false; }
-
-       function formatResult( $skin, $result ) {
-               global $wgContLang;
-
-               $nt = Title::makeTitle( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getPrefixedText() );
-
-               $plink = $skin->makeKnownLinkObj( $nt, htmlspecialchars( $text ) );
-               $wlink = $skin->makeKnownLinkObj( $nt, wfMsgHtml( 'watch' ), 'action=watch' );
-
-               return wfSpecialList( $plink, $wlink );
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialUnwatchedpages() {
-       global $wgUser, $wgOut;
-
-       if ( ! $wgUser->isAllowed( 'unwatchedpages' ) )
-               return $wgOut->permissionRequired( 'unwatchedpages' );
-
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new UnwatchedpagesPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php
deleted file mode 100644 (file)
index 0f37f50..0000000
+++ /dev/null
@@ -1,1755 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-
-/**
- * Entry point
- */
-function wfSpecialUpload() {
-       global $wgRequest;
-       $form = new UploadForm( $wgRequest );
-       $form->execute();
-}
-
-/**
- * implements Special:Upload
- * @ingroup SpecialPage
- */
-class UploadForm {
-       const SUCCESS = 0;
-       const BEFORE_PROCESSING = 1;
-       const LARGE_FILE_SERVER = 2;
-       const EMPTY_FILE = 3;
-       const MIN_LENGHT_PARTNAME = 4;
-       const ILLEGAL_FILENAME = 5;
-       const PROTECTED_PAGE = 6;
-       const OVERWRITE_EXISTING_FILE = 7;
-       const FILETYPE_MISSING = 8;
-       const FILETYPE_BADTYPE = 9;
-       const VERIFICATION_ERROR = 10;
-       const UPLOAD_VERIFICATION_ERROR = 11;
-       const UPLOAD_WARNING = 12;
-       const INTERNAL_ERROR = 13;
-
-       /**#@+
-        * @access private
-        */
-       var $mComment, $mLicense, $mIgnoreWarning, $mCurlError;
-       var $mDestName, $mTempPath, $mFileSize, $mFileProps;
-       var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked;
-       var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType;
-       var $mDestWarningAck, $mCurlDestHandle;
-       var $mLocalFile;
-
-       # Placeholders for text injection by hooks (must be HTML)
-       # extensions should take care to _append_ to the present value
-       var $uploadFormTextTop;
-       var $uploadFormTextAfterSummary;
-
-       const SESSION_VERSION = 1;
-       /**#@-*/
-
-       /**
-        * Constructor : initialise object
-        * Get data POSTed through the form and assign them to the object
-        * @param $request Data posted.
-        */
-       function UploadForm( &$request ) {
-               global $wgAllowCopyUploads;
-               $this->mDesiredDestName   = $request->getText( 'wpDestFile' );
-               $this->mIgnoreWarning     = $request->getCheck( 'wpIgnoreWarning' );
-               $this->mComment           = $request->getText( 'wpUploadDescription' );
-
-               if( !$request->wasPosted() ) {
-                       # GET requests just give the main form; no data except destination
-                       # filename and description
-                       return;
-               }
-
-               # Placeholders for text injection by hooks (empty per default)
-               $this->uploadFormTextTop = "";
-               $this->uploadFormTextAfterSummary = "";
-
-               $this->mReUpload          = $request->getCheck( 'wpReUpload' );
-               $this->mUploadClicked     = $request->getCheck( 'wpUpload' );
-
-               $this->mLicense           = $request->getText( 'wpLicense' );
-               $this->mCopyrightStatus   = $request->getText( 'wpUploadCopyStatus' );
-               $this->mCopyrightSource   = $request->getText( 'wpUploadSource' );
-               $this->mWatchthis         = $request->getBool( 'wpWatchthis' );
-               $this->mSourceType        = $request->getText( 'wpSourceType' );
-               $this->mDestWarningAck    = $request->getText( 'wpDestFileWarningAck' );
-
-               $this->mAction            = $request->getVal( 'action' );
-
-               $this->mSessionKey        = $request->getInt( 'wpSessionKey' );
-               if( !empty( $this->mSessionKey ) &&
-                       isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) &&
-                       $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) {
-                       /**
-                        * Confirming a temporarily stashed upload.
-                        * We don't want path names to be forged, so we keep
-                        * them in the session on the server and just give
-                        * an opaque key to the user agent.
-                        */
-                       $data = $_SESSION['wsUploadData'][$this->mSessionKey];
-                       $this->mTempPath         = $data['mTempPath'];
-                       $this->mFileSize         = $data['mFileSize'];
-                       $this->mSrcName          = $data['mSrcName'];
-                       $this->mFileProps        = $data['mFileProps'];
-                       $this->mCurlError        = 0/*UPLOAD_ERR_OK*/;
-                       $this->mStashed          = true;
-                       $this->mRemoveTempFile   = false;
-               } else {
-                       /**
-                        *Check for a newly uploaded file.
-                        */
-                       if( $wgAllowCopyUploads && $this->mSourceType == 'web' ) {
-                               $this->initializeFromUrl( $request );
-                       } else {
-                               $this->initializeFromUpload( $request );
-                       }
-               }
-       }
-
-       /**
-        * Initialize the uploaded file from PHP data
-        * @access private
-        */
-       function initializeFromUpload( $request ) {
-               $this->mTempPath       = $request->getFileTempName( 'wpUploadFile' );
-               $this->mFileSize       = $request->getFileSize( 'wpUploadFile' );
-               $this->mSrcName        = $request->getFileName( 'wpUploadFile' );
-               $this->mCurlError      = $request->getUploadError( 'wpUploadFile' );
-               $this->mSessionKey     = false;
-               $this->mStashed        = false;
-               $this->mRemoveTempFile = false; // PHP will handle this
-       }
-
-       /**
-        * Copy a web file to a temporary file
-        * @access private
-        */
-       function initializeFromUrl( $request ) {
-               global $wgTmpDirectory;
-               $url = $request->getText( 'wpUploadFileURL' );
-               $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
-
-               $this->mTempPath       = $local_file;
-               $this->mFileSize       = 0; # Will be set by curlCopy
-               $this->mCurlError      = $this->curlCopy( $url, $local_file );
-               $pathParts             = explode( '/', $url );
-               $this->mSrcName        = array_pop( $pathParts );
-               $this->mSessionKey     = false;
-               $this->mStashed        = false;
-
-               // PHP won't auto-cleanup the file
-               $this->mRemoveTempFile = file_exists( $local_file );
-       }
-
-       /**
-        * Safe copy from URL
-        * Returns true if there was an error, false otherwise
-        */
-       private function curlCopy( $url, $dest ) {
-               global $wgUser, $wgOut;
-
-               if( !$wgUser->isAllowed( 'upload_by_url' ) ) {
-                       $wgOut->permissionRequired( 'upload_by_url' );
-                       return true;
-               }
-
-               # Maybe remove some pasting blanks :-)
-               $url =  trim( $url );
-               if( stripos($url, 'http://') !== 0 && stripos($url, 'ftp://') !== 0 ) {
-                       # Only HTTP or FTP URLs
-                       $wgOut->showErrorPage( 'upload-proto-error', 'upload-proto-error-text' );
-                       return true;
-               }
-
-               # Open temporary file
-               $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
-               if( $this->mCurlDestHandle === false ) {
-                       # Could not open temporary file to write in
-                       $wgOut->showErrorPage( 'upload-file-error', 'upload-file-error-text');
-                       return true;
-               }
-
-               $ch = curl_init();
-               curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug
-               curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout
-               curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed
-               curl_setopt( $ch, CURLOPT_URL, $url);
-               curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) );
-               curl_exec( $ch );
-               $error = curl_errno( $ch ) ? true : false;
-               $errornum =  curl_errno( $ch );
-               // if ( $error ) print curl_error ( $ch ) ; # Debugging output
-               curl_close( $ch );
-
-               fclose( $this->mCurlDestHandle );
-               unset( $this->mCurlDestHandle );
-               if( $error ) {
-                       unlink( $dest );
-                       if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) )
-                               $wgOut->showErrorPage( 'upload-misc-error', 'upload-misc-error-text' );
-                       else
-                               $wgOut->showErrorPage( "upload-curl-error$errornum", "upload-curl-error$errornum-text" );
-               }
-
-               return $error;
-       }
-
-       /**
-        * Callback function for CURL-based web transfer
-        * Write data to file unless we've passed the length limit;
-        * if so, abort immediately.
-        * @access private
-        */
-       function uploadCurlCallback( $ch, $data ) {
-               global $wgMaxUploadSize;
-               $length = strlen( $data );
-               $this->mFileSize += $length;
-               if( $this->mFileSize > $wgMaxUploadSize ) {
-                       return 0;
-               }
-               fwrite( $this->mCurlDestHandle, $data );
-               return $length;
-       }
-
-       /**
-        * Start doing stuff
-        * @access public
-        */
-       function execute() {
-               global $wgUser, $wgOut;
-               global $wgEnableUploads;
-
-               # Check uploading enabled
-               if( !$wgEnableUploads ) {
-                       $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
-                       return;
-               }
-
-               # Check permissions
-               if( !$wgUser->isAllowed( 'upload' ) ) {
-                       if( !$wgUser->isLoggedIn() ) {
-                               $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
-                       } else {
-                               $wgOut->permissionRequired( 'upload' );
-                       }
-                       return;
-               }
-
-               # Check blocks
-               if( $wgUser->isBlocked() ) {
-                       $wgOut->blockedPage();
-                       return;
-               }
-
-               if( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-
-               if( $this->mReUpload ) {
-                       if( !$this->unsaveUploadedFile() ) {
-                               return;
-                       }
-                       # Because it is probably checked and shouldn't be
-                       $this->mIgnoreWarning = false;
-                       
-                       $this->mainUploadForm();
-               } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
-                       $this->processUpload();
-               } else {
-                       $this->mainUploadForm();
-               }
-
-               $this->cleanupTempFile();
-       }
-
-       /**
-        * Do the upload
-        * Checks are made in SpecialUpload::execute()
-        *
-        * @access private
-        */
-       function processUpload(){
-               global $wgUser, $wgOut, $wgFileExtensions;
-               $details = null;
-               $value = null;
-               $value = $this->internalProcessUpload( $details );
-
-               switch($value) {
-                       case self::SUCCESS:
-                               $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
-                               break;
-
-                       case self::BEFORE_PROCESSING:
-                               break;
-
-                       case self::LARGE_FILE_SERVER:
-                               $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
-                               break;
-
-                       case self::EMPTY_FILE:
-                               $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
-                               break;
-
-                       case self::MIN_LENGHT_PARTNAME:
-                               $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
-                               break;
-
-                       case self::ILLEGAL_FILENAME:
-                               $filtered = $details['filtered'];
-                               $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
-                               break;
-
-                       case self::PROTECTED_PAGE:
-                               $wgOut->showPermissionsErrorPage( $details['permissionserrors'] );
-                               break;
-
-                       case self::OVERWRITE_EXISTING_FILE:
-                               $errorText = $details['overwrite'];
-                               $overwrite = new WikiError( $wgOut->parse( $errorText ) );
-                               $this->uploadError( $overwrite->toString() );
-                               break;
-
-                       case self::FILETYPE_MISSING:
-                               $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
-                               break;
-
-                       case self::FILETYPE_BADTYPE:
-                               $finalExt = $details['finalExt'];
-                               $this->uploadError(
-                                       wfMsgExt( 'filetype-banned-type',
-                                               array( 'parseinline' ),
-                                               htmlspecialchars( $finalExt ),
-                                               implode(
-                                                       wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
-                                                       $wgFileExtensions
-                                               )
-                                       )
-                               );
-                               break;
-
-                       case self::VERIFICATION_ERROR:
-                               $veri = $details['veri'];
-                               $this->uploadError( $veri->toString() );
-                               break;
-
-                       case self::UPLOAD_VERIFICATION_ERROR:
-                               $error = $details['error'];
-                               $this->uploadError( $error );
-                               break;
-
-                       case self::UPLOAD_WARNING:
-                               $warning = $details['warning'];
-                               $this->uploadWarning( $warning );
-                               break;
-
-                       case self::INTERNAL_ERROR:
-                               $internal = $details['internal'];
-                               $this->showError( $internal );
-                               break;
-
-                       default:
-                               throw new MWException( __METHOD__ . ": Unknown value `{$value}`" );
-               }
-       }
-
-       /**
-        * Really do the upload
-        * Checks are made in SpecialUpload::execute()
-        *
-        * @param array $resultDetails contains result-specific dict of additional values
-        *
-        * @access private
-        */
-       function internalProcessUpload( &$resultDetails ) {
-               global $wgUser;
-
-               if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
-               {
-                       wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
-                       return self::BEFORE_PROCESSING;
-               }
-
-               /**
-                * If there was no filename or a zero size given, give up quick.
-                */
-               if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
-                       return self::EMPTY_FILE;
-               }
-
-               /* Check for curl error */
-               if( $this->mCurlError ) {
-                       return self::BEFORE_PROCESSING;
-               }
-
-               # Chop off any directories in the given filename
-               if( $this->mDesiredDestName ) {
-                       $basename = $this->mDesiredDestName;
-               } else {
-                       $basename = $this->mSrcName;
-               }
-               $filtered = wfBaseName( $basename );
-
-               /**
-                * We'll want to blacklist against *any* 'extension', and use
-                * only the final one for the whitelist.
-                */
-               list( $partname, $ext ) = $this->splitExtensions( $filtered );
-
-               if( count( $ext ) ) {
-                       $finalExt = $ext[count( $ext ) - 1];
-               } else {
-                       $finalExt = '';
-               }
-
-               # If there was more than one "extension", reassemble the base
-               # filename to prevent bogus complaints about length
-               if( count( $ext ) > 1 ) {
-                       for( $i = 0; $i < count( $ext ) - 1; $i++ )
-                               $partname .= '.' . $ext[$i];
-               }
-
-               if( strlen( $partname ) < 1 ) {
-                       return self::MIN_LENGHT_PARTNAME;
-               }
-
-               /**
-                * Filter out illegal characters, and try to make a legible name
-                * out of it. We'll strip some silently that Title would die on.
-                */
-               $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered );
-               $nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
-               if( is_null( $nt ) ) {
-                       $resultDetails = array( 'filtered' => $filtered );
-                       return self::ILLEGAL_FILENAME;
-               }
-               $this->mLocalFile = wfLocalFile( $nt );
-               $this->mDestName = $this->mLocalFile->getName();
-
-               /**
-                * If the image is protected, non-sysop users won't be able
-                * to modify it by uploading a new revision.
-                */
-               $permErrors = $nt->getUserPermissionsErrors( 'edit', $wgUser );
-               $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $wgUser );
-               $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $wgUser ) );
-
-               if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
-                       // merge all the problems into one list, avoiding duplicates
-                       $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
-                       $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
-                       $resultDetails = array( 'permissionserrors' => $permErrors );
-                       return self::PROTECTED_PAGE;
-               }
-
-               /**
-                * In some cases we may forbid overwriting of existing files.
-                */
-               $overwrite = $this->checkOverwrite( $this->mDestName );
-               if( $overwrite !== true ) {
-                       $resultDetails = array( 'overwrite' => $overwrite );
-                       return self::OVERWRITE_EXISTING_FILE;
-               }
-
-               /* Don't allow users to override the blacklist (check file extension) */
-               global $wgCheckFileExtensions, $wgStrictFileExtensions;
-               global $wgFileExtensions, $wgFileBlacklist;
-               if ($finalExt == '') {
-                       return self::FILETYPE_MISSING;
-               } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
-                               ($wgCheckFileExtensions && $wgStrictFileExtensions &&
-                                       !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
-                       $resultDetails = array( 'finalExt' => $finalExt );
-                       return self::FILETYPE_BADTYPE;
-               }
-
-               /**
-                * Look at the contents of the file; if we can recognize the
-                * type but it's corrupt or data of the wrong type, we should
-                * probably not accept it.
-                */
-               if( !$this->mStashed ) {
-                       $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt );
-                       $this->checkMacBinary();
-                       $veri = $this->verify( $this->mTempPath, $finalExt );
-
-                       if( $veri !== true ) { //it's a wiki error...
-                               $resultDetails = array( 'veri' => $veri );
-                               return self::VERIFICATION_ERROR;
-                       }
-
-                       /**
-                        * Provide an opportunity for extensions to add further checks
-                        */
-                       $error = '';
-                       if( !wfRunHooks( 'UploadVerification',
-                                       array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
-                               $resultDetails = array( 'error' => $error );
-                               return self::UPLOAD_VERIFICATION_ERROR;
-                       }
-               }
-
-
-               /**
-                * Check for non-fatal conditions
-                */
-               if ( ! $this->mIgnoreWarning ) {
-                       $warning = '';
-
-                       global $wgCapitalLinks;
-                       if( $wgCapitalLinks ) {
-                               $filtered = ucfirst( $filtered );
-                       }
-                       if( $basename != $filtered ) {
-                               $warning .=  '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'</li>';
-                       }
-
-                       global $wgCheckFileExtensions;
-                       if ( $wgCheckFileExtensions ) {
-                               if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
-                                       $warning .= '<li>' .
-                                       wfMsgExt( 'filetype-unwanted-type',
-                                               array( 'parseinline' ),
-                                               htmlspecialchars( $finalExt ),
-                                               implode(
-                                                       wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
-                                                       $wgFileExtensions
-                                               )
-                                       ) . '</li>';
-                               }
-                       }
-
-                       global $wgUploadSizeWarning;
-                       if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
-                               $skin = $wgUser->getSkin();
-                               $wsize = $skin->formatSize( $wgUploadSizeWarning );
-                               $asize = $skin->formatSize( $this->mFileSize );
-                               $warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>';
-                       }
-                       if ( $this->mFileSize == 0 ) {
-                               $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
-                       }
-
-                       if ( !$this->mDestWarningAck ) {
-                               $warning .= self::getExistsWarning( $this->mLocalFile );
-                       }
-                       
-                       $warning .= $this->getDupeWarning( $this->mTempPath );
-                       
-                       if( $warning != '' ) {
-                               /**
-                                * Stash the file in a temporary location; the user can choose
-                                * to let it through and we'll complete the upload then.
-                                */
-                               $resultDetails = array( 'warning' => $warning );
-                               return self::UPLOAD_WARNING;
-                       }
-               }
-
-               /**
-                * Try actually saving the thing...
-                * It will show an error form on failure.
-                */
-               $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
-                       $this->mCopyrightStatus, $this->mCopyrightSource );
-
-               $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
-                       File::DELETE_SOURCE, $this->mFileProps );
-               if ( !$status->isGood() ) {
-                       $resultDetails = array( 'internal' => $status->getWikiText() );
-                       return self::INTERNAL_ERROR;
-               } else {
-                       if ( $this->mWatchthis ) {
-                               global $wgUser;
-                               $wgUser->addWatch( $this->mLocalFile->getTitle() );
-                       }
-                       // Success, redirect to description page
-                       $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
-                       wfRunHooks( 'UploadComplete', array( &$this ) );
-                       return self::SUCCESS;
-               }
-       }
-
-       /**
-        * Do existence checks on a file and produce a warning
-        * This check is static and can be done pre-upload via AJAX
-        * Returns an HTML fragment consisting of one or more LI elements if there is a warning
-        * Returns an empty string if there is no warning
-        */
-       static function getExistsWarning( $file ) {
-               global $wgUser, $wgContLang;
-               // Check for uppercase extension. We allow these filenames but check if an image
-               // with lowercase extension exists already
-               $warning = '';
-               $align = $wgContLang->isRtl() ? 'left' : 'right';
-
-               if( strpos( $file->getName(), '.' ) == false ) {
-                       $partname = $file->getName();
-                       $rawExtension = '';
-               } else {
-                       $n = strrpos( $file->getName(), '.' );
-                       $rawExtension = substr( $file->getName(), $n + 1 );
-                       $partname = substr( $file->getName(), 0, $n );
-               }
-
-               $sk = $wgUser->getSkin();
-
-               if ( $rawExtension != $file->getExtension() ) {
-                       // We're not using the normalized form of the extension.
-                       // Normal form is lowercase, using most common of alternate
-                       // extensions (eg 'jpg' rather than 'JPEG').
-                       //
-                       // Check for another file using the normalized form...
-                       $nt_lc = Title::makeTitle( NS_IMAGE, $partname . '.' . $file->getExtension() );
-                       $file_lc = wfLocalFile( $nt_lc );
-               } else {
-                       $file_lc = false;
-               }
-
-               if( $file->exists() ) {
-                       $dlink = $sk->makeKnownLinkObj( $file->getTitle() );
-                       if ( $file->allowInlineDisplay() ) {
-                               $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ),
-                                       $file->getName(), $align, array(), false, true );
-                       } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
-                               $icon = $file->iconThumb();
-                               $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
-                                       $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
-                       } else {
-                               $dlink2 = '';
-                       }
-
-                       $warning .= '<li>' . wfMsgExt( 'fileexists', array('parseinline','replaceafter'), $dlink ) . '</li>' . $dlink2;
-
-               } elseif( $file->getTitle()->getArticleID() ) {
-                       $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' );
-                       $warning .= '<li>' . wfMsgExt( 'filepageexists', array( 'parseinline', 'replaceafter' ), $lnk ) . '</li>';
-               } elseif ( $file_lc && $file_lc->exists() ) {
-                       # Check if image with lowercase extension exists.
-                       # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
-                       $dlink = $sk->makeKnownLinkObj( $nt_lc );
-                       if ( $file_lc->allowInlineDisplay() ) {
-                               $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline' ),
-                                       $nt_lc->getText(), $align, array(), false, true );
-                       } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) {
-                               $icon = $file_lc->iconThumb();
-                               $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
-                                       $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
-                       } else {
-                               $dlink2 = '';
-                       }
-
-                       $warning .= '<li>' .
-                               wfMsgExt( 'fileexists-extension', 'parsemag',
-                                       $file->getTitle()->getPrefixedText(), $dlink ) .
-                               '</li>' . $dlink2;
-
-               } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' )
-                       && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) )
-               {
-                       # Check for filenames like 50px- or 180px-, these are mostly thumbnails
-                       $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
-                       $file_thb = wfLocalFile( $nt_thb );
-                       if ($file_thb->exists() ) {
-                               # Check if an image without leading '180px-' (or similiar) exists
-                               $dlink = $sk->makeKnownLinkObj( $nt_thb);
-                               if ( $file_thb->allowInlineDisplay() ) {
-                                       $dlink2 = $sk->makeImageLinkObj( $nt_thb,
-                                               wfMsgExt( 'fileexists-thumb', 'parseinline' ),
-                                               $nt_thb->getText(), $align, array(), false, true );
-                               } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) {
-                                       $icon = $file_thb->iconThumb();
-                                       $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
-                                               $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' .
-                                               $dlink . '</div>';
-                               } else {
-                                       $dlink2 = '';
-                               }
-
-                               $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) .
-                                       '</li>' . $dlink2;
-                       } else {
-                               # Image w/o '180px-' does not exists, but we do not like these filenames
-                               $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' ,
-                                       substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
-                       }
-               }
-
-               $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
-               # Do the match
-               foreach( $filenamePrefixBlacklist as $prefix ) {
-                       if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
-                               $warning .= '<li>' . wfMsgExt( 'filename-bad-prefix', 'parseinline', $prefix ) . '</li>';
-                               break;
-                       }
-               }
-
-               if ( $file->wasDeleted() && !$file->exists() ) {
-                       # If the file existed before and was deleted, warn the user of this
-                       # Don't bother doing so if the file exists now, however
-                       $ltitle = SpecialPage::getTitleFor( 'Log' );
-                       $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
-                               'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
-                       $warning .= '<li>' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '</li>';
-               }
-               return $warning;
-       }
-
-       /**
-        * Get a list of warnings
-        *
-        * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
-        * @return array list of warning messages
-        */
-       static function ajaxGetExistsWarning( $filename ) {
-               $file = wfFindFile( $filename );
-               if( !$file ) {
-                       // Force local file so we have an object to do further checks against
-                       // if there isn't an exact match...
-                       $file = wfLocalFile( $filename );
-               }
-               $s = '&nbsp;';
-               if ( $file ) {
-                       $warning = self::getExistsWarning( $file );
-                       if ( $warning !== '' ) {
-                               $s = "<ul>$warning</ul>";
-                       }
-               }
-               return $s;
-       }
-
-       /**
-        * Render a preview of a given license for the AJAX preview on upload
-        *
-        * @param string $license
-        * @return string
-        */
-       public static function ajaxGetLicensePreview( $license ) {
-               global $wgParser, $wgUser;
-               $text = '{{' . $license . '}}';
-               $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
-               $options = ParserOptions::newFromUser( $wgUser );
-
-               // Expand subst: first, then live templates...
-               $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
-               $output = $wgParser->parse( $text, $title, $options );
-
-               return $output->getText();
-       }
-       
-       /**
-        * Check for duplicate files and throw up a warning before the upload
-        * completes.
-        */
-       function getDupeWarning( $tempfile ) {
-               $hash = File::sha1Base36( $tempfile );
-               $dupes = RepoGroup::singleton()->findBySha1( $hash );
-               if( $dupes ) {
-                       global $wgOut;
-                       $msg = "<gallery>";
-                       foreach( $dupes as $file ) {
-                               $title = $file->getTitle();
-                               $msg .= $title->getPrefixedText() .
-                                       "|" . $title->getText() . "\n";
-                       }
-                       $msg .= "</gallery>";
-                       return "<li>" .
-                               wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) .
-                               $wgOut->parse( $msg ) .
-                               "</li>\n";
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
-        *
-        * @return array list of prefixes
-        */
-       public static function getFilenamePrefixBlacklist() {
-               $blacklist = array();
-               $message = wfMsgForContent( 'filename-prefix-blacklist' );
-               if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
-                       $lines = explode( "\n", $message );
-                       foreach( $lines as $line ) {
-                               // Remove comment lines
-                               $comment = substr( trim( $line ), 0, 1 );
-                               if ( $comment == '#' || $comment == '' ) {
-                                       continue;
-                               }
-                               // Remove additional comments after a prefix
-                               $comment = strpos( $line, '#' );
-                               if ( $comment > 0 ) {
-                                       $line = substr( $line, 0, $comment-1 );
-                               }
-                               $blacklist[] = trim( $line );
-                       }
-               }
-               return $blacklist;
-       }
-
-       /**
-        * Stash a file in a temporary directory for later processing
-        * after the user has confirmed it.
-        *
-        * If the user doesn't explicitly cancel or accept, these files
-        * can accumulate in the temp directory.
-        *
-        * @param string $saveName - the destination filename
-        * @param string $tempName - the source temporary file to save
-        * @return string - full path the stashed file, or false on failure
-        * @access private
-        */
-       function saveTempUploadedFile( $saveName, $tempName ) {
-               global $wgOut;
-               $repo = RepoGroup::singleton()->getLocalRepo();
-               $status = $repo->storeTemp( $saveName, $tempName );
-               if ( !$status->isGood() ) {
-                       $this->showError( $status->getWikiText() );
-                       return false;
-               } else {
-                       return $status->value;
-               }
-       }
-
-       /**
-        * Stash a file in a temporary directory for later processing,
-        * and save the necessary descriptive info into the session.
-        * Returns a key value which will be passed through a form
-        * to pick up the path info on a later invocation.
-        *
-        * @return int
-        * @access private
-        */
-       function stashSession() {
-               $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
-
-               if( !$stash ) {
-                       # Couldn't save the file.
-                       return false;
-               }
-
-               $key = mt_rand( 0, 0x7fffffff );
-               $_SESSION['wsUploadData'][$key] = array(
-                       'mTempPath'       => $stash,
-                       'mFileSize'       => $this->mFileSize,
-                       'mSrcName'        => $this->mSrcName,
-                       'mFileProps'      => $this->mFileProps,
-                       'version'         => self::SESSION_VERSION,
-               );
-               return $key;
-       }
-
-       /**
-        * Remove a temporarily kept file stashed by saveTempUploadedFile().
-        * @access private
-        * @return success
-        */
-       function unsaveUploadedFile() {
-               global $wgOut;
-               $repo = RepoGroup::singleton()->getLocalRepo();
-               $success = $repo->freeTemp( $this->mTempPath );
-               if ( ! $success ) {
-                       $wgOut->showFileDeleteError( $this->mTempPath );
-                       return false;
-               } else {
-                       return true;
-               }
-       }
-
-       /* -------------------------------------------------------------- */
-
-       /**
-        * @param string $error as HTML
-        * @access private
-        */
-       function uploadError( $error ) {
-               global $wgOut;
-               $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
-               $wgOut->addHTML( '<span class="error">' . $error . '</span>' );
-       }
-
-       /**
-        * There's something wrong with this file, not enough to reject it
-        * totally but we require manual intervention to save it for real.
-        * Stash it away, then present a form asking to confirm or cancel.
-        *
-        * @param string $warning as HTML
-        * @access private
-        */
-       function uploadWarning( $warning ) {
-               global $wgOut;
-               global $wgUseCopyrightUpload;
-
-               $this->mSessionKey = $this->stashSession();
-               if( !$this->mSessionKey ) {
-                       # Couldn't save file; an error has been displayed so let's go.
-                       return;
-               }
-
-               $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
-               $wgOut->addHTML( '<ul class="warning">' . $warning . "</ul>\n" );
-
-               $titleObj = SpecialPage::getTitleFor( 'Upload' );
-
-               if ( $wgUseCopyrightUpload ) {
-                       $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" .
-                                       Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n";
-               } else {
-                       $copyright = '';
-               }
-
-               $wgOut->addHTML(
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ),
-                                'enctype' => 'multipart/form-data', 'id' => 'uploadwarning' ) ) . "\n" .
-                       Xml::hidden( 'wpIgnoreWarning', '1' ) . "\n" .
-                       Xml::hidden( 'wpSessionKey', $this->mSessionKey ) . "\n" .
-                       Xml::hidden( 'wpUploadDescription', $this->mComment ) . "\n" .
-                       Xml::hidden( 'wpLicense', $this->mLicense ) . "\n" .
-                       Xml::hidden( 'wpDestFile', $this->mDesiredDestName ) . "\n" .
-                       Xml::hidden( 'wpWatchthis', $this->mWatchthis ) . "\n" .
-                       "{$copyright}<br />" .
-                       Xml::submitButton( wfMsg( 'ignorewarning' ), array ( 'name' => 'wpUpload', 'id' => 'wpUpload', 'checked' => 'checked' ) ) . ' ' .
-                       Xml::submitButton( wfMsg( 'reuploaddesc' ), array ( 'name' => 'wpReUpload', 'id' => 'wpReUpload' ) ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-       }
-
-       /**
-        * Displays the main upload form, optionally with a highlighted
-        * error message up at the top.
-        *
-        * @param string $msg as HTML
-        * @access private
-        */
-       function mainUploadForm( $msg='' ) {
-               global $wgOut, $wgUser, $wgLang, $wgMaxUploadSize;
-               global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview;
-               global $wgRequest, $wgAllowCopyUploads;
-               global $wgStylePath, $wgStyleVersion;
-
-               $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
-               $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview;
-
-               $adc = wfBoolToStr( $useAjaxDestCheck );
-               $alp = wfBoolToStr( $useAjaxLicensePreview );
-               $autofill = wfBoolToStr( $this->mDesiredDestName == '' );
-
-               $wgOut->addScript( "<script type=\"text/javascript\">
-wgAjaxUploadDestCheck = {$adc};
-wgAjaxLicensePreview = {$alp};
-wgUploadAutoFill = {$autofill};
-</script>" );
-               $wgOut->addScriptFile( 'upload.js' );
-               $wgOut->addScriptFile( 'edit.js' ); // For <charinsert> support
-
-               if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) )
-               {
-                       wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
-                       return false;
-               }
-
-               if( $this->mDesiredDestName ) {
-                       $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName );
-                       // Show a subtitle link to deleted revisions (to sysops et al only)
-                       if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) {
-                               $link = wfMsgExt(
-                                       $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
-                                       array( 'parse', 'replaceafter' ),
-                                       $wgUser->getSkin()->makeKnownLinkObj(
-                                               SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
-                                               wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
-                                       )
-                               );
-                               $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
-                       }
-
-                       // Show the relevant lines from deletion log (for still deleted files only)
-                       if( $title instanceof Title && $title->isDeleted() > 0 && !$title->exists() ) {
-                               $this->showDeletionLog( $wgOut, $title->getPrefixedText() );
-                       }
-               }
-
-               $cols = intval($wgUser->getOption( 'cols' ));
-
-               if( $wgUser->getOption( 'editwidth' ) ) {
-                       $width = " style=\"width:100%\"";
-               } else {
-                       $width = '';
-               }
-
-               if ( '' != $msg ) {
-                       $sub = wfMsgHtml( 'uploaderror' );
-                       $wgOut->addHTML( "<h2>{$sub}</h2>\n" .
-                         "<span class='error'>{$msg}</span>\n" );
-               }
-               $wgOut->addHTML( '<div id="uploadtext">' );
-               $wgOut->addWikiMsg( 'uploadtext', $this->mDesiredDestName );
-               $wgOut->addHTML( "</div>\n" );
-
-               # Print a list of allowed file extensions, if so configured.  We ignore
-               # MIME type here, it's incomprehensible to most people and too long.
-               global $wgCheckFileExtensions, $wgStrictFileExtensions,
-               $wgFileExtensions, $wgFileBlacklist;
-
-               $allowedExtensions = '';
-               if( $wgCheckFileExtensions ) {
-                       $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
-                       if( $wgStrictFileExtensions ) {
-                               # Everything not permitted is banned
-                               $extensionsList =
-                                       '<div id="mw-upload-permitted">' .
-                                       wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) .
-                                       "</div>\n";
-                       } else {
-                               # We have to list both preferred and prohibited
-                               $extensionsList =
-                                       '<div id="mw-upload-preferred">' .
-                                       wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) .
-                                       "</div>\n" .
-                                       '<div id="mw-upload-prohibited">' .
-                                       wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) .
-                                       "</div>\n";
-                       }
-               } else {
-                       # Everything is permitted.
-                       $extensionsList = '';
-               }
-
-               # Get the maximum file size from php.ini as $wgMaxUploadSize works for uploads from URL via CURL only
-               # See http://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize for possible values of upload_max_filesize
-               $val = trim( ini_get( 'upload_max_filesize' ) );
-               $last = strtoupper( ( substr( $val, -1 ) ) );
-               switch( $last ) {
-                       case 'G':
-                               $val2 = substr( $val, 0, -1 ) * 1024 * 1024 * 1024;
-                               break;
-                       case 'M':
-                               $val2 = substr( $val, 0, -1 ) * 1024 * 1024;
-                               break;
-                       case 'K':
-                               $val2 = substr( $val, 0, -1 ) * 1024;
-                               break;
-                       default:
-                               $val2 = $val;
-               }
-               $val2 = $wgAllowCopyUploads ? min( $wgMaxUploadSize, $val2 ) : $val2;
-               $maxUploadSize = '<div id="mw-upload-maxfilesize">' . 
-                       wfMsgExt( 'upload-maxfilesize', array( 'parseinline', 'escapenoentities' ), 
-                               $wgLang->formatSize( $val2 ) ) .
-                               "</div>\n";
-
-               $sourcefilename = wfMsgExt( 'sourcefilename', array( 'parseinline', 'escapenoentities' ) );
-        $destfilename = wfMsgExt( 'destfilename', array( 'parseinline', 'escapenoentities' ) ); 
-               
-               $summary = wfMsgExt( 'fileuploadsummary', 'parseinline' );
-
-               $licenses = new Licenses();
-               $license = wfMsgExt( 'license', array( 'parseinline' ) );
-               $nolicense = wfMsgHtml( 'nolicense' );
-               $licenseshtml = $licenses->getHtml();
-
-               $ulb = wfMsgHtml( 'uploadbtn' );
-
-
-               $titleObj = SpecialPage::getTitleFor( 'Upload' );
-
-               $encDestName = htmlspecialchars( $this->mDesiredDestName );
-
-               $watchChecked = $this->watchCheck()
-                       ? 'checked="checked"'
-                       : '';
-               $warningChecked = $this->mIgnoreWarning ? 'checked' : '';
-
-               // Prepare form for upload or upload/copy
-               if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) {
-                       $filename_form =
-                               "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' " .
-                                  "onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked='checked' />" .
-                                "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
-                                  "onfocus='" .
-                                    "toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");" .
-                                    "toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")' " .
-                                    "onchange='fillDestFilename(\"wpUploadFile\")' size='60' />" .
-                               wfMsgHTML( 'upload_source_file' ) . "<br/>" .
-                               "<input type='radio' id='wpSourceTypeURL' name='wpSourceType' value='web' " .
-                                 "onchange='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\")' />" .
-                               "<input tabindex='1' type='text' name='wpUploadFileURL' id='wpUploadFileURL' " .
-                                 "onfocus='" .
-                                   "toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\");" .
-                                   "toggle_element_check(\"wpSourceTypeURL\",\"wpSourceTypeFile\")' " .
-                                   "onchange='fillDestFilename(\"wpUploadFileURL\")' size='60' disabled='disabled' />" .
-                               wfMsgHtml( 'upload_source_url' ) ;
-               } else {
-                       $filename_form =
-                               "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
-                               ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") .
-                               "size='60' />" .
-                               "<input type='hidden' name='wpSourceType' value='file' />" ;
-               }
-               if ( $useAjaxDestCheck ) {
-                       $warningRow = "<tr><td colspan='2' id='wpDestFile-warning'>&nbsp;</td></tr>";
-                       $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"';
-               } else {
-                       $warningRow = '';
-                       $destOnkeyup = '';
-               }
-
-               $encComment = htmlspecialchars( $this->mComment );
-
-               $wgOut->addHTML(
-                        Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL(),
-                                'enctype' => 'multipart/form-data', 'id' => 'mw-upload-form' ) ) .
-                        Xml::openElement( 'fieldset' ) .
-                        Xml::element( 'legend', null, wfMsg( 'upload' ) ) .
-                        Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-upload-table' ) ) .
-                        "<tr>
-                               {$this->uploadFormTextTop}
-                               <td class='mw-label'>
-                                       <label for='wpUploadFile'>{$sourcefilename}</label>
-                               </td>
-                               <td class='mw-input'>
-                                       {$filename_form}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td></td>
-                               <td>
-                                       {$maxUploadSize}
-                                       {$extensionsList}
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class='mw-label'>
-                                       <label for='wpDestFile'>{$destfilename}</label>
-                               </td>
-                               <td class='mw-input'>
-                                       <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='60'
-                                               value=\"{$encDestName}\" onchange='toggleFilenameFiller()' $destOnkeyup />
-                               </td>
-                       </tr>
-                       <tr>
-                               <td class='mw-label'>
-                                       <label for='wpUploadDescription'>{$summary}</label>
-                               </td>
-                               <td class='mw-input'>
-                                       <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6'
-                                               cols='{$cols}'{$width}>$encComment</textarea>
-                                       {$this->uploadFormTextAfterSummary}
-                               </td>
-                       </tr>
-                       <tr>"
-               );
-
-               if ( $licenseshtml != '' ) {
-                       global $wgStylePath;
-                       $wgOut->addHTML( "
-                                       <td class='mw-label'>
-                                               <label for='wpLicense'>$license</label>
-                                       </td>
-                                       <td class='mw-input'>
-                                               <select name='wpLicense' id='wpLicense' tabindex='4'
-                                                       onchange='licenseSelectorCheck()'>
-                                                       <option value=''>$nolicense</option>
-                                                       $licenseshtml
-                                               </select>
-                                       </td>
-                               </tr>
-                               <tr>"
-                       );
-                       if( $useAjaxLicensePreview ) {
-                               $wgOut->addHtml( "
-                                               <td></td>
-                                               <td id=\"mw-license-preview\"></td>
-                                       </tr>
-                                       <tr>"
-                               );
-                       }
-               }
-
-               if ( $wgUseCopyrightUpload ) {
-                       $filestatus = wfMsgExt( 'filestatus', 'escapenoentities' );
-                       $copystatus =  htmlspecialchars( $this->mCopyrightStatus );
-                       $filesource = wfMsgExt( 'filesource', 'escapenoentities' );
-                       $uploadsource = htmlspecialchars( $this->mCopyrightSource );
-
-                       $wgOut->addHTML( "
-                                       <td class='mw-label' style='white-space: nowrap;'>
-                                               <label for='wpUploadCopyStatus'>$filestatus</label></td>
-                                       <td class='mw-input'>
-                                               <input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus'
-                                                       value=\"$copystatus\" size='60' />
-                                       </td>
-                               </tr>
-                               <tr>
-                                       <td class='mw-label'>
-                                               <label for='wpUploadCopyStatus'>$filesource</label>
-                                       </td>
-                                       <td class='mw-input'>
-                                               <input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus'
-                                                       value=\"$uploadsource\" size='60' />
-                                       </td>
-                               </tr>
-                               <tr>"
-                       );
-               }
-
-               $wgOut->addHtml( "
-                               <td></td>
-                               <td>
-                                       <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
-                                       <label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label>
-                                       <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' $warningChecked/>
-                                       <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
-                               </td>
-                       </tr>
-                       $warningRow
-                       <tr>
-                               <td></td>
-                                       <td class='mw-input'>
-                                               <input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\"" . $wgUser->getSkin()->tooltipAndAccesskey( 'upload' ) . " />
-                                       </td>
-                       </tr>
-                       <tr>
-                               <td></td>
-                               <td class='mw-input'>"
-               );
-               $wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
-               $wgOut->addHTML( "
-                               </td>
-                       </tr>" .
-                       Xml::closeElement( 'table' ) .
-                       Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' )
-               );
-               $uploadfooter = wfMsgNoTrans( 'uploadfooter' );
-               if( $uploadfooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadfooter ) ){
-                       $wgOut->addWikiText( '<div id="mw-upload-footer-message">' . $uploadfooter . '</div>' );
-               }
-       }
-
-       /* -------------------------------------------------------------- */
-       
-       /**
-        * See if we should check the 'watch this page' checkbox on the form
-        * based on the user's preferences and whether we're being asked
-        * to create a new file or update an existing one.
-        *
-        * In the case where 'watch edits' is off but 'watch creations' is on,
-        * we'll leave the box unchecked.
-        *
-        * Note that the page target can be changed *on the form*, so our check
-        * state can get out of sync.
-        */
-       function watchCheck() {
-               global $wgUser;
-               if( $wgUser->getOption( 'watchdefault' ) ) {
-                       // Watch all edits!
-                       return true;
-               }
-               
-               $local = wfLocalFile( $this->mDesiredDestName );
-               if( $local && $local->exists() ) {
-                       // We're uploading a new version of an existing file.
-                       // No creation, so don't watch it if we're not already.
-                       return $local->getTitle()->userIsWatching();
-               } else {
-                       // New page should get watched if that's our option.
-                       return $wgUser->getOption( 'watchcreations' );
-               }
-       }
-
-       /**
-        * Split a file into a base name and all dot-delimited 'extensions'
-        * on the end. Some web server configurations will fall back to
-        * earlier pseudo-'extensions' to determine type and execute
-        * scripts, so the blacklist needs to check them all.
-        *
-        * @return array
-        */
-       function splitExtensions( $filename ) {
-               $bits = explode( '.', $filename );
-               $basename = array_shift( $bits );
-               return array( $basename, $bits );
-       }
-
-       /**
-        * Perform case-insensitive match against a list of file extensions.
-        * Returns true if the extension is in the list.
-        *
-        * @param string $ext
-        * @param array $list
-        * @return bool
-        */
-       function checkFileExtension( $ext, $list ) {
-               return in_array( strtolower( $ext ), $list );
-       }
-
-       /**
-        * Perform case-insensitive match against a list of file extensions.
-        * Returns true if any of the extensions are in the list.
-        *
-        * @param array $ext
-        * @param array $list
-        * @return bool
-        */
-       function checkFileExtensionList( $ext, $list ) {
-               foreach( $ext as $e ) {
-                       if( in_array( strtolower( $e ), $list ) ) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-
-       /**
-        * Verifies that it's ok to include the uploaded file
-        *
-        * @param string $tmpfile the full path of the temporary file to verify
-        * @param string $extension The filename extension that the file is to be served with
-        * @return mixed true of the file is verified, a WikiError object otherwise.
-        */
-       function verify( $tmpfile, $extension ) {
-               #magically determine mime type
-               $magic = MimeMagic::singleton();
-               $mime = $magic->guessMimeType($tmpfile,false);
-
-               #check mime type, if desired
-               global $wgVerifyMimeType;
-               if ($wgVerifyMimeType) {
-
-                 wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
-                       #check mime type against file extension
-                       if( !$this->verifyExtension( $mime, $extension ) ) {
-                               return new WikiErrorMsg( 'uploadcorrupt' );
-                       }
-
-                       #check mime type blacklist
-                       global $wgMimeTypeBlacklist;
-                       if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
-                               && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
-                               return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
-                       }
-               }
-
-               #check for htmlish code and javascript
-               if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
-                       return new WikiErrorMsg( 'uploadscripted' );
-               }
-
-               /**
-               * Scan the uploaded file for viruses
-               */
-               $virus= $this->detectVirus($tmpfile);
-               if ( $virus ) {
-                       return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
-               }
-
-               wfDebug( __METHOD__.": all clear; passing.\n" );
-               return true;
-       }
-
-       /**
-        * Checks if the mime type of the uploaded file matches the file extension.
-        *
-        * @param string $mime the mime type of the uploaded file
-        * @param string $extension The filename extension that the file is to be served with
-        * @return bool
-        */
-       function verifyExtension( $mime, $extension ) {
-               $magic = MimeMagic::singleton();
-
-               if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
-                       if ( ! $magic->isRecognizableExtension( $extension ) ) {
-                               wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
-                                       "unrecognized extension '$extension', can't verify\n" );
-                               return true;
-                       } else {
-                               wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
-                                       "recognized extension '$extension', so probably invalid file\n" );
-                               return false;
-                       }
-
-               $match= $magic->isMatchingExtension($extension,$mime);
-
-               if ($match===NULL) {
-                       wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
-                       return true;
-               } elseif ($match===true) {
-                       wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
-
-                       #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
-                       return true;
-
-               } else {
-                       wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
-                       return false;
-               }
-       }
-
-       /**
-        * Heuristic for detecting files that *could* contain JavaScript instructions or
-        * things that may look like HTML to a browser and are thus
-        * potentially harmful. The present implementation will produce false positives in some situations.
-        *
-        * @param string $file Pathname to the temporary upload file
-        * @param string $mime The mime type of the file
-        * @param string $extension The extension of the file
-        * @return bool true if the file contains something looking like embedded scripts
-        */
-       function detectScript($file, $mime, $extension) {
-               global $wgAllowTitlesInSVG;
-
-               #ugly hack: for text files, always look at the entire file.
-               #For binarie field, just check the first K.
-
-               if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
-               else {
-                       $fp = fopen( $file, 'rb' );
-                       $chunk = fread( $fp, 1024 );
-                       fclose( $fp );
-               }
-
-               $chunk= strtolower( $chunk );
-
-               if (!$chunk) return false;
-
-               #decode from UTF-16 if needed (could be used for obfuscation).
-               if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
-               elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
-               else $enc= NULL;
-
-               if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
-
-               $chunk= trim($chunk);
-
-               #FIXME: convert from UTF-16 if necessarry!
-
-               wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
-
-               #check for HTML doctype
-               if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
-
-               /**
-               * Internet Explorer for Windows performs some really stupid file type
-               * autodetection which can cause it to interpret valid image files as HTML
-               * and potentially execute JavaScript, creating a cross-site scripting
-               * attack vectors.
-               *
-               * Apple's Safari browser also performs some unsafe file type autodetection
-               * which can cause legitimate files to be interpreted as HTML if the
-               * web server is not correctly configured to send the right content-type
-               * (or if you're really uploading plain text and octet streams!)
-               *
-               * Returns true if IE is likely to mistake the given file for HTML.
-               * Also returns true if Safari would mistake the given file for HTML
-               * when served with a generic content-type.
-               */
-
-               $tags = array(
-                       '<body',
-                       '<head',
-                       '<html',   #also in safari
-                       '<img',
-                       '<pre',
-                       '<script', #also in safari
-                       '<table'
-                       );
-               if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
-                       $tags[] = '<title';
-               }
-
-               foreach( $tags as $tag ) {
-                       if( false !== strpos( $chunk, $tag ) ) {
-                               return true;
-                       }
-               }
-
-               /*
-               * look for javascript
-               */
-
-               #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
-               $chunk = Sanitizer::decodeCharReferences( $chunk );
-
-               #look for script-types
-               if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true;
-
-               #look for html-style script-urls
-               if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
-
-               #look for css-style script-urls
-               if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
-
-               wfDebug("SpecialUpload::detectScript: no scripts found\n");
-               return false;
-       }
-
-       /**
-        * Generic wrapper function for a virus scanner program.
-        * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
-        * $wgAntivirusRequired may be used to deny upload if the scan fails.
-        *
-        * @param string $file Pathname to the temporary upload file
-        * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
-        *         or a string containing feedback from the virus scanner if a virus was found.
-        *         If textual feedback is missing but a virus was found, this function returns true.
-        */
-       function detectVirus($file) {
-               global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
-
-               if ( !$wgAntivirus ) {
-                       wfDebug( __METHOD__.": virus scanner disabled\n");
-                       return NULL;
-               }
-
-               if ( !$wgAntivirusSetup[$wgAntivirus] ) {
-                       wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" );
-                       # @TODO: localise
-                       $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" );
-                       return "unknown antivirus: $wgAntivirus";
-               }
-
-               # look up scanner configuration
-               $command = $wgAntivirusSetup[$wgAntivirus]["command"];
-               $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
-               $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
-                       $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
-
-               if ( strpos( $command,"%f" ) === false ) {
-                       # simple pattern: append file to scan
-                       $command .= " " . wfEscapeShellArg( $file );
-               } else {
-                       # complex pattern: replace "%f" with file to scan
-                       $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
-               }
-
-               wfDebug( __METHOD__.": running virus scan: $command \n" );
-
-               # execute virus scanner
-               $exitCode = false;
-
-               #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
-               #      that does not seem to be worth the pain.
-               #      Ask me (Duesentrieb) about it if it's ever needed.
-               $output = array();
-               if ( wfIsWindows() ) {
-                       exec( "$command", $output, $exitCode );
-               } else {
-                       exec( "$command 2>&1", $output, $exitCode );
-               }
-
-               # map exit code to AV_xxx constants.
-               $mappedCode = $exitCode;
-               if ( $exitCodeMap ) {
-                       if ( isset( $exitCodeMap[$exitCode] ) ) {
-                               $mappedCode = $exitCodeMap[$exitCode];
-                       } elseif ( isset( $exitCodeMap["*"] ) ) {
-                               $mappedCode = $exitCodeMap["*"];
-                       }
-               }
-
-               if ( $mappedCode === AV_SCAN_FAILED ) {
-                       # scan failed (code was mapped to false by $exitCodeMap)
-                       wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
-
-                       if ( $wgAntivirusRequired ) {
-                               return "scan failed (code $exitCode)";
-                       } else {
-                               return NULL;
-                       }
-               } else if ( $mappedCode === AV_SCAN_ABORTED ) {
-                       # scan failed because filetype is unknown (probably imune)
-                       wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
-                       return NULL;
-               } else if ( $mappedCode === AV_NO_VIRUS ) {
-                       # no virus found
-                       wfDebug( __METHOD__.": file passed virus scan.\n" );
-                       return false;
-               } else {
-                       $output = join( "\n", $output );
-                       $output = trim( $output );
-
-                       if ( !$output ) {
-                               $output = true; #if there's no output, return true
-                       } elseif ( $msgPattern ) {
-                               $groups = array();
-                               if ( preg_match( $msgPattern, $output, $groups ) ) {
-                                       if ( $groups[1] ) {
-                                               $output = $groups[1];
-                                       }
-                               }
-                       }
-
-                       wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
-                       return $output;
-               }
-       }
-
-       /**
-        * Check if the temporary file is MacBinary-encoded, as some uploads
-        * from Internet Explorer on Mac OS Classic and Mac OS X will be.
-        * If so, the data fork will be extracted to a second temporary file,
-        * which will then be checked for validity and either kept or discarded.
-        *
-        * @access private
-        */
-       function checkMacBinary() {
-               $macbin = new MacBinary( $this->mTempPath );
-               if( $macbin->isValid() ) {
-                       $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
-                       $dataHandle = fopen( $dataFile, 'wb' );
-
-                       wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
-                       $macbin->extractData( $dataHandle );
-
-                       $this->mTempPath = $dataFile;
-                       $this->mFileSize = $macbin->dataForkLength();
-
-                       // We'll have to manually remove the new file if it's not kept.
-                       $this->mRemoveTempFile = true;
-               }
-               $macbin->close();
-       }
-
-       /**
-        * If we've modified the upload file we need to manually remove it
-        * on exit to clean up.
-        * @access private
-        */
-       function cleanupTempFile() {
-               if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
-                       wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
-                       unlink( $this->mTempPath );
-               }
-       }
-
-       /**
-        * Check if there's an overwrite conflict and, if so, if restrictions
-        * forbid this user from performing the upload.
-        *
-        * @return mixed true on success, WikiError on failure
-        * @access private
-        */
-       function checkOverwrite( $name ) {
-               $img = wfFindFile( $name );
-
-               $error = '';
-               if( $img ) {
-                       global $wgUser, $wgOut;
-                       if( $img->isLocal() ) {
-                               if( !self::userCanReUpload( $wgUser, $img->name ) ) {
-                                       $error = 'fileexists-forbidden';
-                               }
-                       } else {
-                               if( !$wgUser->isAllowed( 'reupload' ) ||
-                                   !$wgUser->isAllowed( 'reupload-shared' ) ) {
-                                       $error = "fileexists-shared-forbidden";
-                               }
-                       }
-               }
-
-               if( $error ) {
-                       $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
-                       return $errorText;
-               }
-
-               // Rockin', go ahead and upload
-               return true;
-       }
-
-        /**
-        * Check if a user is the last uploader
-        *
-        * @param User $user
-        * @param string $img, image name
-        * @return bool
-        */
-       public static function userCanReUpload( User $user, $img ) {
-               if( $user->isAllowed( 'reupload' ) )
-                       return true; // non-conditional
-               if( !$user->isAllowed( 'reupload-own' ) )
-                       return false;
-
-               $dbr = wfGetDB( DB_SLAVE );
-               $row = $dbr->selectRow('image',
-               /* SELECT */ 'img_user',
-               /* WHERE */ array( 'img_name' => $img )
-               );
-               if ( !$row )
-                       return false;
-
-               return $user->getId() == $row->img_user;
-       }
-
-       /**
-        * Display an error with a wikitext description
-        */
-       function showError( $description ) {
-               global $wgOut;
-               $wgOut->setPageTitle( wfMsg( "internalerror" ) );
-               $wgOut->setRobotpolicy( "noindex,nofollow" );
-               $wgOut->setArticleRelated( false );
-               $wgOut->enableClientCache( false );
-               $wgOut->addWikiText( $description );
-       }
-
-       /**
-        * Get the initial image page text based on a comment and optional file status information
-        */
-       static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
-               global $wgUseCopyrightUpload;
-               if ( $wgUseCopyrightUpload ) {
-                       if ( $license != '' ) {
-                               $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
-                       }
-                       $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
-                         '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
-                         "$licensetxt" .
-                         '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
-               } else {
-                       if ( $license != '' ) {
-                               $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
-                                $pageText = $filedesc .
-                                        '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
-                       } else {
-                               $pageText = $comment;
-                       }
-               }
-               return $pageText;
-       }
-
-       /**
-        * If there are rows in the deletion log for this file, show them,
-        * along with a nice little note for the user
-        *
-        * @param OutputPage $out
-        * @param string filename
-        */
-       private function showDeletionLog( $out, $filename ) {
-               global $wgUser;
-               $loglist = new LogEventsList( $wgUser->getSkin(), $out );
-               $pager = new LogPager( $loglist, 'delete', false, $filename );
-               if( $pager->getNumRows() > 0 ) {
-                       $out->addHtml( '<div id="mw-upload-deleted-warn">' );
-                       $out->addWikiMsg( 'upload-wasdeleted' );
-                       $out->addHTML(
-                               $loglist->beginLogEventsList() .
-                               $pager->getBody() .
-                               $loglist->endLogEventsList()
-                       );
-                       $out->addHtml( '</div>' );
-               }
-       }
-}
diff --git a/includes/SpecialUploadMogile.php b/includes/SpecialUploadMogile.php
deleted file mode 100644 (file)
index 7ff8fda..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * You will need the extension MogileClient to use this special page.
- */
-require_once( 'MogileFS.php' );
-
-/**
- * Entry point
- */
-function wfSpecialUploadMogile() {
-       global $wgRequest;
-       $form = new UploadFormMogile( $wgRequest );
-       $form->execute();
-}
-
-/**
- * Extends Special:Upload with MogileFS.
- * @ingroup SpecialPage
- */
-class UploadFormMogile extends UploadForm {
-       /**
-        * Move the uploaded file from its temporary location to the final
-        * destination. If a previous version of the file exists, move
-        * it into the archive subdirectory.
-        *
-        * @todo If the later save fails, we may have disappeared the original file.
-        *
-        * @param string $saveName
-        * @param string $tempName full path to the temporary file
-        * @param bool $useRename  Not used in this implementation
-        */
-       function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
-               global $wgOut;
-               $mfs = MogileFS::NewMogileFS();
-
-               $this->mSavedFile = "image!{$saveName}";
-
-               if( $mfs->getPaths( $this->mSavedFile )) {
-                       $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
-                       if( !$mfs->rename( $this->mSavedFile, "archive!{$this->mUploadOldVersion}" ) ) {
-                               $wgOut->showFileRenameError( $this->mSavedFile,
-                                 "archive!{$this->mUploadOldVersion}" );
-                               return false;
-                       }
-               } else {
-                       $this->mUploadOldVersion = '';
-               }
-
-               if ( $this->mStashed ) {
-                       if (!$mfs->rename($tempName,$this->mSavedFile)) {
-                               $wgOut->showFileRenameError($tempName, $this->mSavedFile );
-                               return false;
-                       }
-               } else {
-                       if ( !$mfs->saveFile($this->mSavedFile,'normal',$tempName )) {
-                               $wgOut->showFileCopyError( $tempName, $this->mSavedFile );
-                               return false;
-                       }
-                       unlink($tempName);
-               }
-               return true;
-       }
-
-       /**
-        * Stash a file in a temporary directory for later processing
-        * after the user has confirmed it.
-        *
-        * If the user doesn't explicitly cancel or accept, these files
-        * can accumulate in the temp directory.
-        *
-        * @param string $saveName - the destination filename
-        * @param string $tempName - the source temporary file to save
-        * @return string - full path the stashed file, or false on failure
-        * @access private
-        */
-       function saveTempUploadedFile( $saveName, $tempName ) {
-               global $wgOut;
-
-               $stash = 'stash!' . gmdate( "YmdHis" ) . '!' . $saveName;
-               $mfs = MogileFS::NewMogileFS();
-               if ( !$mfs->saveFile( $stash, 'normal', $tempName ) ) {
-                       $wgOut->showFileCopyError( $tempName, $stash );
-                       return false;
-               }
-               unlink($tempName);
-               return $stash;
-       }
-
-       /**
-        * Stash a file in a temporary directory for later processing,
-        * and save the necessary descriptive info into the session.
-        * Returns a key value which will be passed through a form
-        * to pick up the path info on a later invocation.
-        *
-        * @return int
-        * @access private
-        */
-       function stashSession() {
-               $stash = $this->saveTempUploadedFile(
-                       $this->mUploadSaveName, $this->mUploadTempName );
-
-               if( !$stash ) {
-                       # Couldn't save the file.
-                       return false;
-               }
-
-               $key = mt_rand( 0, 0x7fffffff );
-               $_SESSION['wsUploadData'][$key] = array(
-                       'mUploadTempName' => $stash,
-                       'mUploadSize'     => $this->mUploadSize,
-                       'mOname'          => $this->mOname );
-               return $key;
-       }
-
-       /**
-        * Remove a temporarily kept file stashed by saveTempUploadedFile().
-        * @access private
-        * @return success
-        */
-       function unsaveUploadedFile() {
-               global $wgOut;
-               $mfs = MogileFS::NewMogileFS();
-               if ( ! $mfs->delete( $this->mUploadTempName ) ) {
-                       $wgOut->showFileDeleteError( $this->mUploadTempName );
-                       return false;
-               } else {
-                       return true;
-               }
-       }
-}
diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php
deleted file mode 100644 (file)
index 179ef3f..0000000
+++ /dev/null
@@ -1,928 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * constructor
- */
-function wfSpecialUserlogin( $par = '' ) {
-       global $wgRequest;
-       if( session_id() == '' ) {
-               wfSetupSession();
-       }
-
-       $form = new LoginForm( $wgRequest, $par );
-       $form->execute();
-}
-
-/**
- * implements Special:Login
- * @ingroup SpecialPage
- */
-class LoginForm {
-
-       const SUCCESS = 0;
-       const NO_NAME = 1;
-       const ILLEGAL = 2;
-       const WRONG_PLUGIN_PASS = 3;
-       const NOT_EXISTS = 4;
-       const WRONG_PASS = 5;
-       const EMPTY_PASS = 6;
-       const RESET_PASS = 7;
-       const ABORTED = 8;
-       const CREATE_BLOCKED = 9;
-
-       var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
-       var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
-       var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage, $mSkipCookieCheck;
-
-       /**
-        * Constructor
-        * @param WebRequest $request A WebRequest object passed by reference
-        */
-       function LoginForm( &$request, $par = '' ) {
-               global $wgLang, $wgAllowRealName, $wgEnableEmail;
-               global $wgAuth;
-
-               $this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]]
-               $this->mName = $request->getText( 'wpName' );
-               $this->mPassword = $request->getText( 'wpPassword' );
-               $this->mRetype = $request->getText( 'wpRetype' );
-               $this->mDomain = $request->getText( 'wpDomain' );
-               $this->mReturnTo = $request->getVal( 'returnto' );
-               $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
-               $this->mPosted = $request->wasPosted();
-               $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
-               $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
-                                           && $wgEnableEmail;
-               $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
-                                        && $wgEnableEmail;
-               $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
-               $this->mAction = $request->getVal( 'action' );
-               $this->mRemember = $request->getCheck( 'wpRemember' );
-               $this->mLanguage = $request->getText( 'uselang' );
-               $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
-
-               if( $wgEnableEmail ) {
-                       $this->mEmail = $request->getText( 'wpEmail' );
-               } else {
-                       $this->mEmail = '';
-               }
-               if( $wgAllowRealName ) {
-                   $this->mRealName = $request->getText( 'wpRealName' );
-               } else {
-                   $this->mRealName = '';
-               }
-
-               if( !$wgAuth->validDomain( $this->mDomain ) ) {
-                       $this->mDomain = 'invaliddomain';
-               }
-               $wgAuth->setDomain( $this->mDomain );
-
-               # When switching accounts, it sucks to get automatically logged out
-               if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) {
-                       $this->mReturnTo = '';
-               }
-       }
-
-       function execute() {
-               if ( !is_null( $this->mCookieCheck ) ) {
-                       $this->onCookieRedirectCheck( $this->mCookieCheck );
-                       return;
-               } else if( $this->mPosted ) {
-                       if( $this->mCreateaccount ) {
-                               return $this->addNewAccount();
-                       } else if ( $this->mCreateaccountMail ) {
-                               return $this->addNewAccountMailPassword();
-                       } else if ( $this->mMailmypassword ) {
-                               return $this->mailPassword();
-                       } else if ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
-                               return $this->processLogin();
-                       }
-               }
-               $this->mainLoginForm( '' );
-       }
-
-       /**
-        * @private
-        */
-       function addNewAccountMailPassword() {
-               global $wgOut;
-
-               if ('' == $this->mEmail) {
-                       $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) );
-                       return;
-               }
-
-               $u = $this->addNewaccountInternal();
-
-               if ($u == NULL) {
-                       return;
-               }
-
-               // Wipe the initial password and mail a temporary one
-               $u->setPassword( null );
-               $u->saveSettings();
-               $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
-
-               wfRunHooks( 'AddNewAccount', array( $u, true ) );
-
-               $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->setArticleRelated( false );
-
-               if( WikiError::isError( $result ) ) {
-                       $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
-               } else {
-                       $wgOut->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
-                       $wgOut->returnToMain( false );
-               }
-               $u = 0;
-       }
-
-
-       /**
-        * @private
-        */
-       function addNewAccount() {
-               global $wgUser, $wgEmailAuthentication;
-
-               # Create the account and abort if there's a problem doing so
-               $u = $this->addNewAccountInternal();
-               if( $u == NULL )
-                       return;
-
-               # If we showed up language selection links, and one was in use, be
-               # smart (and sensible) and save that language as the user's preference
-               global $wgLoginLanguageSelector;
-               if( $wgLoginLanguageSelector && $this->mLanguage )
-                       $u->setOption( 'language', $this->mLanguage );
-
-               # Send out an email authentication message if needed
-               if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) ) {
-                       global $wgOut;
-                       $error = $u->sendConfirmationMail();
-                       if( WikiError::isError( $error ) ) {
-                               $wgOut->addWikiMsg( 'confirmemail_sendfailed', $error->getMessage() );
-                       } else {
-                               $wgOut->addWikiMsg( 'confirmemail_oncreate' );
-                       }
-               }
-
-               # Save settings (including confirmation token)
-               $u->saveSettings();
-
-               # If not logged in, assume the new account as the current one and set session cookies
-               # then show a "welcome" message or a "need cookies" message as needed
-               if( $wgUser->isAnon() ) {
-                       $wgUser = $u;
-                       $wgUser->setCookies();
-                       wfRunHooks( 'AddNewAccount', array( $wgUser ) );
-                       if( $this->hasSessionCookie() ) {
-                               return $this->successfulLogin( wfMsg( 'welcomecreation', $wgUser->getName() ), false );
-                       } else {
-                               return $this->cookieRedirectCheck( 'new' );
-                       }
-               } else {
-                       # Confirm that the account was created
-                       global $wgOut;
-                       $self = SpecialPage::getTitleFor( 'Userlogin' );
-                       $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) );
-                       $wgOut->setArticleRelated( false );
-                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
-                       $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
-                       $wgOut->returnToMain( false, $self );
-                       wfRunHooks( 'AddNewAccount', array( $u ) );
-                       return true;
-               }
-       }
-
-       /**
-        * @private
-        */
-       function addNewAccountInternal() {
-               global $wgUser, $wgOut;
-               global $wgEnableSorbs, $wgProxyWhitelist;
-               global $wgMemc, $wgAccountCreationThrottle;
-               global $wgAuth, $wgMinimalPasswordLength;
-               global $wgEmailConfirmToEdit;
-
-               // If the user passes an invalid domain, something is fishy
-               if( !$wgAuth->validDomain( $this->mDomain ) ) {
-                       $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
-                       return false;
-               }
-
-               // If we are not allowing users to login locally, we should
-               // be checking to see if the user is actually able to
-               // authenticate to the authentication server before they
-               // create an account (otherwise, they can create a local account
-               // and login as any domain user). We only need to check this for
-               // domains that aren't local.
-               if( 'local' != $this->mDomain && '' != $this->mDomain ) {
-                       if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
-                               $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
-                               return false;
-                       }
-               }
-
-               if ( wfReadOnly() ) {
-                       $wgOut->readOnlyPage();
-                       return false;
-               }
-
-               # Check permissions
-               if ( !$wgUser->isAllowed( 'createaccount' ) ) {
-                       $this->userNotPrivilegedMessage();
-                       return false;
-               } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
-                       $this->userBlockedMessage();
-                       return false;
-               }
-
-               $ip = wfGetIP();
-               if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
-                 $wgUser->inSorbsBlacklist( $ip ) )
-               {
-                       $this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' );
-                       return;
-               }
-
-               # Now create a dummy user ($u) and check if it is valid
-               $name = trim( $this->mName );
-               $u = User::newFromName( $name, 'creatable' );
-               if ( is_null( $u ) ) {
-                       $this->mainLoginForm( wfMsg( 'noname' ) );
-                       return false;
-               }
-
-               if ( 0 != $u->idForName() ) {
-                       $this->mainLoginForm( wfMsg( 'userexists' ) );
-                       return false;
-               }
-
-               if ( 0 != strcmp( $this->mPassword, $this->mRetype ) ) {
-                       $this->mainLoginForm( wfMsg( 'badretype' ) );
-                       return false;
-               }
-
-               # check for minimal password length
-               if ( !$u->isValidPassword( $this->mPassword ) ) {
-                       if ( !$this->mCreateaccountMail ) {
-                               $this->mainLoginForm( wfMsgExt( 'passwordtooshort', array( 'parsemag' ), $wgMinimalPasswordLength ) );
-                               return false;
-                       } else {
-                               # do not force a password for account creation by email
-                               # set invalid password, it will be replaced later by a random generated password
-                               $this->mPassword = null;
-                       }
-               }
-
-               # if you need a confirmed email address to edit, then obviously you need an email address.
-               if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) {
-                       $this->mainLoginForm( wfMsg( 'noemailtitle' ) );
-                       return false;
-               }
-
-               if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
-                       $this->mainLoginForm( wfMsg( 'invalidemailaddress' ) );
-                       return false;
-               }
-
-               # Set some additional data so the AbortNewAccount hook can be
-               # used for more than just username validation
-               $u->setEmail( $this->mEmail );
-               $u->setRealName( $this->mRealName );
-
-               $abortError = '';
-               if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
-                       // Hook point to add extra creation throttles and blocks
-                       wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
-                       $this->mainLoginForm( $abortError );
-                       return false;
-               }
-
-               if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
-                       $key = wfMemcKey( 'acctcreate', 'ip', $ip );
-                       $value = $wgMemc->incr( $key );
-                       if ( !$value ) {
-                               $wgMemc->set( $key, 1, 86400 );
-                       }
-                       if ( $value > $wgAccountCreationThrottle ) {
-                               $this->throttleHit( $wgAccountCreationThrottle );
-                               return false;
-                       }
-               }
-
-               if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
-                       $this->mainLoginForm( wfMsg( 'externaldberror' ) );
-                       return false;
-               }
-
-               return $this->initUser( $u, false );
-       }
-
-       /**
-        * Actually add a user to the database.
-        * Give it a User object that has been initialised with a name.
-        *
-        * @param $u User object.
-        * @param $autocreate boolean -- true if this is an autocreation via auth plugin
-        * @return User object.
-        * @private
-        */
-       function initUser( $u, $autocreate ) {
-               global $wgAuth;
-
-               $u->addToDatabase();
-
-               if ( $wgAuth->allowPasswordChange() ) {
-                       $u->setPassword( $this->mPassword );
-               }
-
-               $u->setEmail( $this->mEmail );
-               $u->setRealName( $this->mRealName );
-               $u->setToken();
-
-               $wgAuth->initUser( $u, $autocreate );
-
-               $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
-               $u->saveSettings();
-
-               # Update user count
-               $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
-               $ssUpdate->doUpdate();
-
-               return $u;
-       }
-
-       /**
-        * Internally authenticate the login request.
-        *
-        * This may create a local account as a side effect if the
-        * authentication plugin allows transparent local account
-        * creation.
-        *
-        * @public
-        */
-       function authenticateUserData() {
-               global $wgUser, $wgAuth;
-               if ( '' == $this->mName ) {
-                       return self::NO_NAME;
-               }
-
-               // Load $wgUser now, and check to see if we're logging in as the same name. 
-               // This is necessary because loading $wgUser (say by calling getName()) calls
-               // the UserLoadFromSession hook, which potentially creates the user in the 
-               // database. Until we load $wgUser, checking for user existence using 
-               // User::newFromName($name)->getId() below will effectively be using stale data.
-               if ( $wgUser->getName() === $this->mName ) {
-                       wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
-                       return self::SUCCESS;
-               }
-               $u = User::newFromName( $this->mName );
-               if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) {
-                       return self::ILLEGAL;
-               }
-
-               $isAutoCreated = false;
-               if ( 0 == $u->getID() ) {
-                       $status = $this->attemptAutoCreate( $u );
-                       if ( $status !== self::SUCCESS ) {
-                               return $status;
-                       } else {
-                               $isAutoCreated = true;
-                       }
-               } else {
-                       $u->load();
-               }
-
-               // Give general extensions, such as a captcha, a chance to abort logins
-               $abort = self::ABORTED;
-               if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort ) ) ) {
-                       return $abort;
-               }
-
-               if (!$u->checkPassword( $this->mPassword )) {
-                       if( $u->checkTemporaryPassword( $this->mPassword ) ) {
-                               // The e-mailed temporary password should not be used
-                               // for actual logins; that's a very sloppy habit,
-                               // and insecure if an attacker has a few seconds to
-                               // click "search" on someone's open mail reader.
-                               //
-                               // Allow it to be used only to reset the password
-                               // a single time to a new value, which won't be in
-                               // the user's e-mail archives.
-                               //
-                               // For backwards compatibility, we'll still recognize
-                               // it at the login form to minimize surprises for
-                               // people who have been logging in with a temporary
-                               // password for some time.
-                               //
-                               // As a side-effect, we can authenticate the user's
-                               // e-mail address if it's not already done, since
-                               // the temporary password was sent via e-mail.
-                               //
-                               if( !$u->isEmailConfirmed() ) {
-                                       $u->confirmEmail();
-                                       $u->saveSettings();
-                               }
-
-                               // At this point we just return an appropriate code
-                               // indicating that the UI should show a password
-                               // reset form; bot interfaces etc will probably just
-                               // fail cleanly here.
-                               //
-                               $retval = self::RESET_PASS;
-                       } else {
-                               $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
-                       }
-               } else {
-                       $wgAuth->updateUser( $u );
-                       $wgUser = $u;
-
-                       if ( $isAutoCreated ) {
-                               // Must be run after $wgUser is set, for correct new user log
-                               wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
-                       }
-
-                       $retval = self::SUCCESS;
-               }
-               wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) );
-               return $retval;
-       }
-
-       /**
-        * Attempt to automatically create a user on login.
-        * Only succeeds if there is an external authentication method which allows it.
-        * @return integer Status code
-        */
-       function attemptAutoCreate( $user ) {
-               global $wgAuth, $wgUser;
-               /**
-                * If the external authentication plugin allows it,
-                * automatically create a new account for users that
-                * are externally defined but have not yet logged in.
-                */
-               if ( !$wgAuth->autoCreate() ) {
-                       return self::NOT_EXISTS;
-               }
-               if ( !$wgAuth->userExists( $user->getName() ) ) {
-                       wfDebug( __METHOD__.": user does not exist\n" );
-                       return self::NOT_EXISTS;
-               }
-               if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
-                       wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
-                       return self::WRONG_PLUGIN_PASS;
-               }
-               if ( $wgUser->isBlockedFromCreateAccount() ) {
-                       wfDebug( __METHOD__.": user is blocked from account creation\n" );
-                       return self::CREATE_BLOCKED;
-               }
-
-               wfDebug( __METHOD__.": creating account\n" );
-               $user = $this->initUser( $user, true );
-               return self::SUCCESS;
-       }
-
-       function processLogin() {
-               global $wgUser, $wgAuth;
-
-               switch ($this->authenticateUserData())
-               {
-                       case self::SUCCESS:
-                               # We've verified now, update the real record
-                               if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
-                                       $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
-                                       $wgUser->saveSettings();
-                               } else {
-                                       $wgUser->invalidateCache();
-                               }
-                               $wgUser->setCookies();
-
-                               if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
-                                       /* Replace the language object to provide user interface in correct
-                                        * language immediately on this first page load.
-                                        */
-                                       global $wgLang, $wgRequest;
-                                       $code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) );
-                                       $wgLang = Language::factory( $code );
-                                       return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) );
-                               } else {
-                                       return $this->cookieRedirectCheck( 'login' );
-                               }
-                               break;
-
-                       case self::NO_NAME:
-                       case self::ILLEGAL:
-                               $this->mainLoginForm( wfMsg( 'noname' ) );
-                               break;
-                       case self::WRONG_PLUGIN_PASS:
-                               $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
-                               break;
-                       case self::NOT_EXISTS:
-                               if( $wgUser->isAllowed( 'createaccount' ) ){
-                                       $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
-                               } else {
-                                       $this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) );
-                               }
-                               break;
-                       case self::WRONG_PASS:
-                               $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
-                               break;
-                       case self::EMPTY_PASS:
-                               $this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) );
-                               break;
-                       case self::RESET_PASS:
-                               $this->resetLoginForm( wfMsg( 'resetpass_announce' ) );
-                               break;
-                       case self::CREATE_BLOCKED:
-                               $this->userBlockedMessage();
-                               break;
-                       default:
-                               throw new MWException( "Unhandled case value" );
-               }
-       }
-
-       function resetLoginForm( $error ) {
-               global $wgOut;
-               $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
-               $reset = new PasswordResetForm( $this->mName, $this->mPassword );
-               $reset->execute( null );
-       }
-
-       /**
-        * @private
-        */
-       function mailPassword() {
-               global $wgUser, $wgOut, $wgAuth;
-
-               if( !$wgAuth->allowPasswordChange() ) {
-                       $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
-                       return;
-               }
-
-               # Check against blocked IPs
-               # fixme -- should we not?
-               if( $wgUser->isBlocked() ) {
-                       $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
-                       return;
-               }
-
-               # Check against the rate limiter
-               if( $wgUser->pingLimiter( 'mailpassword' ) ) {
-                       $wgOut->rateLimited();
-                       return;
-               }
-
-               if ( '' == $this->mName ) {
-                       $this->mainLoginForm( wfMsg( 'noname' ) );
-                       return;
-               }
-               $u = User::newFromName( $this->mName );
-               if( is_null( $u ) ) {
-                       $this->mainLoginForm( wfMsg( 'noname' ) );
-                       return;
-               }
-               if ( 0 == $u->getID() ) {
-                       $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) );
-                       return;
-               }
-
-               # Check against password throttle
-               if ( $u->isPasswordReminderThrottled() ) {
-                       global $wgPasswordReminderResendTime;
-                       # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds.
-                       $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
-                               round( $wgPasswordReminderResendTime, 3 ) ) );
-                       return;
-               }
-
-               $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
-               if( WikiError::isError( $result ) ) {
-                       $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
-               } else {
-                       $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
-               }
-       }
-
-
-       /**
-        * @param object user
-        * @param bool throttle
-        * @param string message name of email title
-        * @param string message name of email text
-        * @return mixed true on success, WikiError on failure
-        * @private
-        */
-       function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
-               global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
-               global $wgServer, $wgScript;
-
-               if ( '' == $u->getEmail() ) {
-                       return new WikiError( wfMsg( 'noemail', $u->getName() ) );
-               }
-
-               $np = $u->randomPassword();
-               $u->setNewpassword( $np, $throttle );
-               $u->saveSettings();
-
-               $ip = wfGetIP();
-               if ( '' == $ip ) { $ip = '(Unknown)'; }
-
-               $m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript );
-               $result = $u->sendMail( wfMsg( $emailTitle ), $m );
-
-               return $result;
-       }
-
-
-       /**
-        * @param string $msg Message that will be shown on success
-        * @param bool $auto Toggle auto-redirect to main page; default true
-        * @private
-        */
-       function successfulLogin( $msg, $auto = true ) {
-               global $wgUser;
-               global $wgOut;
-
-               # Run any hooks; ignore results
-
-               $injected_html = '';
-               wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
-
-               $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->setArticleRelated( false );
-               $wgOut->addWikiText( $msg );
-               $wgOut->addHtml( $injected_html );
-               if ( !empty( $this->mReturnTo ) ) {
-                       $wgOut->returnToMain( $auto, $this->mReturnTo );
-               } else {
-                       $wgOut->returnToMain( $auto );
-               }
-       }
-
-       /** */
-       function userNotPrivilegedMessage($errors) {
-               global $wgOut;
-
-               $wgOut->setPageTitle( wfMsg( 'permissionserrors' ) );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->setArticleRelated( false );
-
-               $wgOut->addWikitext( $wgOut->formatPermissionsErrorMessage( $errors, 'createaccount' ) );
-               // Stuff that might want to be added at the end. For example, instructions if blocked.
-               $wgOut->addWikiMsg( 'cantcreateaccount-nonblock-text' );
-
-               $wgOut->returnToMain( false );
-       }
-
-       /** */
-       function userBlockedMessage() {
-               global $wgOut, $wgUser;
-
-               # Let's be nice about this, it's likely that this feature will be used
-               # for blocking large numbers of innocent people, e.g. range blocks on
-               # schools. Don't blame it on the user. There's a small chance that it
-               # really is the user's fault, i.e. the username is blocked and they
-               # haven't bothered to log out before trying to create an account to
-               # evade it, but we'll leave that to their guilty conscience to figure
-               # out.
-
-               $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->setArticleRelated( false );
-
-               $ip = wfGetIP();
-               $blocker = User::whoIs( $wgUser->mBlock->mBy );
-               $block_reason = $wgUser->mBlock->mReason;
-
-               if ( strval( $block_reason ) === '' ) {
-                       $block_reason = wfMsg( 'blockednoreason' );
-               }
-               $wgOut->addWikiMsg( 'cantcreateaccount-text', $ip, $block_reason, $blocker );
-               $wgOut->returnToMain( false );
-       }
-
-       /**
-        * @private
-        */
-       function mainLoginForm( $msg, $msgtype = 'error' ) {
-               global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
-               global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
-               global $wgAuth, $wgEmailConfirmToEdit;
-               
-               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
-               
-               if ( $this->mType == 'signup' ) {
-                       // Block signup here if in readonly. Keeps user from 
-                       // going through the process (filling out data, etc) 
-                       // and being informed later.
-                       if ( wfReadOnly() ) {
-                               $wgOut->readOnlyPage();
-                               return;
-                       } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
-                               $this->userBlockedMessage();
-                               return;
-                       } elseif ( count( $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $wgUser, true ) )>0 ) {
-                               $wgOut->showPermissionsErrorPage( $permErrors, 'createaccount' );
-                               return;
-                       }
-               }
-
-               if ( '' == $this->mName ) {
-                       if ( $wgUser->isLoggedIn() ) {
-                               $this->mName = $wgUser->getName();
-                       } else {
-                               $this->mName = isset( $_COOKIE[$wgCookiePrefix.'UserName'] ) ? $_COOKIE[$wgCookiePrefix.'UserName'] : null;
-                       }
-               }
-
-               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
-
-               if ( $this->mType == 'signup' ) {
-                       $template = new UsercreateTemplate();
-                       $q = 'action=submitlogin&type=signup';
-                       $linkq = 'type=login';
-                       $linkmsg = 'gotaccount';
-               } else {
-                       $template = new UserloginTemplate();
-                       $q = 'action=submitlogin&type=login';
-                       $linkq = 'type=signup';
-                       $linkmsg = 'nologin';
-               }
-
-               if ( !empty( $this->mReturnTo ) ) {
-                       $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
-                       $q .= $returnto;
-                       $linkq .= $returnto;
-               }
-
-               # Pass any language selection on to the mode switch link
-               if( $wgLoginLanguageSelector && $this->mLanguage )
-                       $linkq .= '&uselang=' . $this->mLanguage;
-
-               $link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">';
-               $link .= wfMsgHtml( $linkmsg . 'link' ); # Calling either 'gotaccountlink' or 'nologinlink'
-               $link .= '</a>';
-
-               # Don't show a "create account" link if the user can't
-               if( $this->showCreateOrLoginLink( $wgUser ) )
-                       $template->set( 'link', wfMsgHtml( $linkmsg, $link ) );
-               else
-                       $template->set( 'link', '' );
-
-               $template->set( 'header', '' );
-               $template->set( 'name', $this->mName );
-               $template->set( 'password', $this->mPassword );
-               $template->set( 'retype', $this->mRetype );
-               $template->set( 'email', $this->mEmail );
-               $template->set( 'realname', $this->mRealName );
-               $template->set( 'domain', $this->mDomain );
-
-               $template->set( 'action', $titleObj->getLocalUrl( $q ) );
-               $template->set( 'message', $msg );
-               $template->set( 'messagetype', $msgtype );
-               $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
-               $template->set( 'userealname', $wgAllowRealName );
-               $template->set( 'useemail', $wgEnableEmail );
-               $template->set( 'emailrequired', $wgEmailConfirmToEdit );
-               $template->set( 'canreset', $wgAuth->allowPasswordChange() );
-               $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember  );
-
-               # Prepare language selection links as needed
-               if( $wgLoginLanguageSelector ) {
-                       $template->set( 'languages', $this->makeLanguageSelector() );
-                       if( $this->mLanguage )
-                               $template->set( 'uselang', $this->mLanguage );
-               }
-
-               // Give authentication and captcha plugins a chance to modify the form
-               $wgAuth->modifyUITemplate( $template );
-               if ( $this->mType == 'signup' ) {
-                       wfRunHooks( 'UserCreateForm', array( &$template ) );
-               } else {
-                       wfRunHooks( 'UserLoginForm', array( &$template ) );
-               }
-
-               $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
-               $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->setArticleRelated( false );
-               $wgOut->disallowUserJs();  // just in case...
-               $wgOut->addTemplate( $template );
-       }
-
-       /**
-        * @private
-        */
-       function showCreateOrLoginLink( &$user ) {
-               if( $this->mType == 'signup' ) {
-                       return( true );
-               } elseif( $user->isAllowed( 'createaccount' ) ) {
-                       return( true );
-               } else {
-                       return( false );
-               }
-       }
-
-       /**
-        * Check if a session cookie is present.
-        *
-        * This will not pick up a cookie set during _this_ request, but is
-        * meant to ensure that the client is returning the cookie which was
-        * set on a previous pass through the system.
-        *
-        * @private
-        */
-       function hasSessionCookie() {
-               global $wgDisableCookieCheck, $wgRequest;
-               return $wgDisableCookieCheck ? true : $wgRequest->checkSessionCookie();
-       }
-
-       /**
-        * @private
-        */
-       function cookieRedirectCheck( $type ) {
-               global $wgOut;
-
-               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
-               $check = $titleObj->getFullURL( 'wpCookieCheck='.$type );
-
-               return $wgOut->redirect( $check );
-       }
-
-       /**
-        * @private
-        */
-       function onCookieRedirectCheck( $type ) {
-               global $wgUser;
-
-               if ( !$this->hasSessionCookie() ) {
-                       if ( $type == 'new' ) {
-                               return $this->mainLoginForm( wfMsgExt( 'nocookiesnew', array( 'parseinline' ) ) );
-                       } else if ( $type == 'login' ) {
-                               return $this->mainLoginForm( wfMsgExt( 'nocookieslogin', array( 'parseinline' ) ) );
-                       } else {
-                               # shouldn't happen
-                               return $this->mainLoginForm( wfMsg( 'error' ) );
-                       }
-               } else {
-                       return $this->successfulLogin( wfMsgExt( 'loginsuccess', array( 'parseinline' ), $wgUser->getName() ) );
-               }
-       }
-
-       /**
-        * @private
-        */
-       function throttleHit( $limit ) {
-               global $wgOut;
-
-               $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit );
-       }
-
-       /**
-        * Produce a bar of links which allow the user to select another language
-        * during login/registration but retain "returnto"
-        *
-        * @return string
-        */
-       function makeLanguageSelector() {
-               $msg = wfMsgForContent( 'loginlanguagelinks' );
-               if( $msg != '' && !wfEmptyMsg( 'loginlanguagelinks', $msg ) ) {
-                       $langs = explode( "\n", $msg );
-                       $links = array();
-                       foreach( $langs as $lang ) {
-                               $lang = trim( $lang, '* ' );
-                               $parts = explode( '|', $lang );
-                               if (count($parts) >= 2) {
-                                       $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
-                               }
-                       }
-                       return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : '';
-               } else {
-                       return '';
-               }
-       }
-
-       /**
-        * Create a language selector link for a particular language
-        * Links back to this page preserving type and returnto
-        *
-        * @param $text Link text
-        * @param $lang Language code
-        */
-       function makeLanguageSelectorLink( $text, $lang ) {
-               global $wgUser;
-               $self = SpecialPage::getTitleFor( 'Userlogin' );
-               $attr[] = 'uselang=' . $lang;
-               if( $this->mType == 'signup' )
-                       $attr[] = 'type=signup';
-               if( $this->mReturnTo )
-                       $attr[] = 'returnto=' . $this->mReturnTo;
-               $skin = $wgUser->getSkin();
-               return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
-       }
-}
diff --git a/includes/SpecialUserlogout.php b/includes/SpecialUserlogout.php
deleted file mode 100644 (file)
index 137eadb..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * constructor
- */
-function wfSpecialUserlogout() {
-       global $wgUser, $wgOut;
-
-       $oldName = $wgUser->getName();
-       $wgUser->logout();
-       $wgOut->setRobotpolicy( 'noindex,nofollow' );
-
-       // Hook.
-       $injected_html = '';
-       wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) );
-
-       $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) . $injected_html );
-       $wgOut->returnToMain();
-}
diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php
deleted file mode 100644 (file)
index bf4d440..0000000
+++ /dev/null
@@ -1,576 +0,0 @@
-<?php
-/**
- * Special page to allow managing user group membership
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A class to manage user levels rights.
- * @ingroup SpecialPage
- */
-class UserrightsPage extends SpecialPage {
-       # The target of the local right-adjuster's interest.  Can be gotten from
-       # either a GET parameter or a subpage-style parameter, so have a member
-       # variable for it.
-       protected $mTarget;
-       protected $isself = false;
-
-       public function __construct() {
-               parent::__construct( 'Userrights' );
-       }
-
-       public function isRestricted() {
-               return true;
-       }
-
-       public function userCanExecute( $user ) {
-               $available = $this->changeableGroups();
-               return !empty( $available['add'] )
-                       or !empty( $available['remove'] )
-                       or ($this->isself and
-                               (!empty( $available['add-self'] )
-                                or !empty( $available['remove-self'] )));
-       }
-
-       /**
-        * Manage forms to be shown according to posted data.
-        * Depending on the submit button used, call a form or a save function.
-        *
-        * @param $par Mixed: string if any subpage provided, else null
-        */
-       function execute( $par ) {
-               // If the visitor doesn't have permissions to assign or remove
-               // any groups, it's a bit silly to give them the user search prompt.
-               global $wgUser, $wgRequest;
-
-               if( $par ) {
-                       $this->mTarget = $par;
-               } else {
-                       $this->mTarget = $wgRequest->getVal( 'user' );
-               }
-
-               if (!$this->mTarget) {
-                       /*
-                        * If the user specified no target, and they can only
-                        * edit their own groups, automatically set them as the
-                        * target.
-                        */
-                       $available = $this->changeableGroups();
-                       if (empty($available['add']) && empty($available['remove']))
-                               $this->mTarget = $wgUser->getName();
-               }
-
-               if ($this->mTarget == $wgUser->getName())
-                       $this->isself = true;
-
-               if( !$this->userCanExecute( $wgUser ) ) {
-                       // fixme... there may be intermediate groups we can mention.
-                       global $wgOut;
-                       $wgOut->showPermissionsErrorPage( array(
-                               $wgUser->isAnon()
-                                       ? 'userrights-nologin'
-                                       : 'userrights-notallowed' ) );
-                       return;
-               }
-
-               if ( wfReadOnly() ) {
-                       global $wgOut;
-                       $wgOut->readOnlyPage();
-                       return;
-               }
-
-               $this->outputHeader();
-
-               $this->setHeaders();
-
-               // show the general form
-               $this->switchForm();
-
-               if( $wgRequest->wasPosted() ) {
-                       // save settings
-                       if( $wgRequest->getCheck( 'saveusergroups' ) ) {
-                               $reason = $wgRequest->getVal( 'user-reason' );
-                               if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $this->mTarget ) ) {
-                                       $this->saveUserGroups(
-                                               $this->mTarget,
-                                               $reason
-                                       );
-                               }
-                       }
-               }
-
-               // show some more forms
-               if( $this->mTarget ) {
-                       $this->editUserGroupsForm( $this->mTarget );
-               }
-       }
-
-       /**
-        * Save user groups changes in the database.
-        * Data comes from the editUserGroupsForm() form function
-        *
-        * @param $username String: username to apply changes to.
-        * @param $reason String: reason for group change
-        * @return null
-        */
-       function saveUserGroups( $username, $reason = '') {
-               global $wgRequest, $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
-
-               $user = $this->fetchUser( $username );
-               if( !$user ) {
-                       return;
-               }
-
-               $allgroups = $this->getAllGroups();
-               $addgroup = array();
-               $removegroup = array();
-
-               // This could possibly create a highly unlikely race condition if permissions are changed between
-               //  when the form is loaded and when the form is saved. Ignoring it for the moment.
-               foreach ($allgroups as $group) {
-                       // We'll tell it to remove all unchecked groups, and add all checked groups.
-                       // Later on, this gets filtered for what can actually be removed
-                       if ($wgRequest->getCheck( "wpGroup-$group" )) {
-                               $addgroup[] = $group;
-                       } else {
-                               $removegroup[] = $group;
-                       }
-               }
-
-               // Validate input set...
-               $changeable = $this->changeableGroups();
-               if ($wgUser->getId() != 0 && $wgUser->getId() == $user->getId()) {
-                       $addable = array_merge($changeable['add'], $wgGroupsAddToSelf);
-                       $removable = array_merge($changeable['remove'], $wgGroupsRemoveFromSelf);
-               } else {
-                       $addable = $changeable['add'];
-                       $removable = $changeable['remove'];
-               }
-
-               $removegroup = array_unique(
-                       array_intersect( (array)$removegroup, $removable ) );
-               $addgroup = array_unique(
-                       array_intersect( (array)$addgroup, $addable ) );
-
-               $oldGroups = $user->getGroups();
-               $newGroups = $oldGroups;
-               // remove then add groups
-               if( $removegroup ) {
-                       $newGroups = array_diff($newGroups, $removegroup);
-                       foreach( $removegroup as $group ) {
-                               $user->removeGroup( $group );
-                       }
-               }
-               if( $addgroup ) {
-                       $newGroups = array_merge($newGroups, $addgroup);
-                       foreach( $addgroup as $group ) {
-                               $user->addGroup( $group );
-                       }
-               }
-               $newGroups = array_unique( $newGroups );
-
-               // Ensure that caches are cleared
-               $user->invalidateCache();
-
-               wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
-               wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
-               if( $user instanceof User ) {
-                       // hmmm
-                       wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
-               }
-
-               if( $newGroups != $oldGroups ) {
-                       $this->addLogEntry( $user, $oldGroups, $newGroups );
-               }
-       }
-       
-       /**
-        * Add a rights log entry for an action.
-        */
-       function addLogEntry( $user, $oldGroups, $newGroups ) {
-               global $wgRequest;
-               $log = new LogPage( 'rights' );
-
-               $log->addEntry( 'rights',
-                       $user->getUserPage(),
-                       $wgRequest->getText( 'user-reason' ),
-                       array(
-                               $this->makeGroupNameList( $oldGroups ),
-                               $this->makeGroupNameList( $newGroups )
-                       )
-               );
-       }
-
-       /**
-        * Edit user groups membership
-        * @param $username String: name of the user.
-        */
-       function editUserGroupsForm( $username ) {
-               global $wgOut;
-
-               $user = $this->fetchUser( $username );
-               if( !$user ) {
-                       return;
-               }
-
-               $groups = $user->getGroups();
-
-               $this->showEditUserGroupsForm( $user, $groups );
-
-               // This isn't really ideal logging behavior, but let's not hide the
-               // interwiki logs if we're using them as is.
-               $this->showLogFragment( $user, $wgOut );
-       }
-
-       /**
-        * Normalize the input username, which may be local or remote, and
-        * return a user (or proxy) object for manipulating it.
-        *
-        * Side effects: error output for invalid access
-        * @return mixed User, UserRightsProxy, or null
-        */
-       function fetchUser( $username ) {
-               global $wgOut, $wgUser;
-
-               $parts = explode( '@', $username );
-               if( count( $parts ) < 2 ) {
-                       $name = trim( $username );
-                       $database = '';
-               } else {
-                       list( $name, $database ) = array_map( 'trim', $parts );
-
-                       if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) {
-                               $wgOut->addWikiMsg( 'userrights-no-interwiki' );
-                               return null;
-                       }
-                       if( !UserRightsProxy::validDatabase( $database ) ) {
-                               $wgOut->addWikiMsg( 'userrights-nodatabase', $database );
-                               return null;
-                       }
-               }
-
-               if( $name == '' ) {
-                       $wgOut->addWikiMsg( 'nouserspecified' );
-                       return false;
-               }
-
-               if( $name{0} == '#' ) {
-                       // Numeric ID can be specified...
-                       // We'll do a lookup for the name internally.
-                       $id = intval( substr( $name, 1 ) );
-
-                       if( $database == '' ) {
-                               $name = User::whoIs( $id );
-                       } else {
-                               $name = UserRightsProxy::whoIs( $database, $id );
-                       }
-
-                       if( !$name ) {
-                               $wgOut->addWikiMsg( 'noname' );
-                               return null;
-                       }
-               }
-
-               if( $database == '' ) {
-                       $user = User::newFromName( $name );
-               } else {
-                       $user = UserRightsProxy::newFromName( $database, $name );
-               }
-
-               if( !$user || $user->isAnon() ) {
-                       $wgOut->addWikiMsg( 'nosuchusershort', $username );
-                       return null;
-               }
-
-               return $user;
-       }
-
-       function makeGroupNameList( $ids ) {
-               return implode( ', ', $ids );
-       }
-
-       /**
-        * Output a form to allow searching for a user
-        */
-       function switchForm() {
-               global $wgOut, $wgScript;
-               $wgOut->addHTML(
-                       Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
-                       Xml::hidden( 'title',  $this->getTitle()->getPrefixedText() ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', array(), wfMsg( 'userrights-lookup-user' ) ) .
-                       Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, $this->mTarget ) . ' ' .
-                       Xml::submitButton( wfMsg( 'editusergroup' ) ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-       }
-
-       /**
-        * Go through used and available groups and return the ones that this
-        * form will be able to manipulate based on the current user's system
-        * permissions.
-        *
-        * @param $groups Array: list of groups the given user is in
-        * @return Array:  Tuple of addable, then removable groups
-        */
-       protected function splitGroups( $groups ) {
-               global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
-               list($addable, $removable) = array_values( $this->changeableGroups() );
-
-               $removable = array_intersect(
-                               array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable),
-                               $groups ); // Can't remove groups the user doesn't have
-               $addable   = array_diff(
-                               array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable),
-                               $groups ); // Can't add groups the user does have
-
-               return array( $addable, $removable );
-       }
-
-       /**
-        * Show the form to edit group memberships.
-        *
-        * @param $user      User or UserRightsProxy you're editing
-        * @param $groups    Array:  Array of groups the user is in
-        */
-       protected function showEditUserGroupsForm( $user, $groups ) {
-               global $wgOut, $wgUser;
-
-               list( $addable, $removable ) = $this->splitGroups( $groups );
-
-               $list = array();
-               foreach( $user->getGroups() as $group )
-                       $list[] = self::buildGroupLink( $group );
-
-               $grouplist = '';
-               if( count( $list ) > 0 ) {
-                       $grouplist = Xml::tags( 'p', null, wfMsgHtml( 'userrights-groupsmember' ) . ' ' . implode( ', ', $list ) );
-               }
-               $wgOut->addHTML(
-                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
-                       Xml::hidden( 'user', $this->mTarget ) .
-                       Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->mTarget ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
-                       wfMsgExt( 'editinguser', array( 'parse' ), wfEscapeWikiText( $user->getName() ) ) .
-                       wfMsgExt( 'userrights-groups-help', array( 'parse' ) ) .
-                       $grouplist .
-                       Xml::tags( 'p', null, $this->groupCheckboxes( $groups ) ) .
-                       Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) .
-                               "<tr>
-                                       <td class='mw-label'>" .
-                                               Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
-                                       "</td>
-                                       <td class='mw-input'>" .
-                                               Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
-                                       "</td>
-                               </tr>
-                               <tr>
-                                       <td></td>
-                                       <td class='mw-submit'>" .
-                                               Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
-                                       "</td>
-                               </tr>" .
-                       Xml::closeElement( 'table' ) . "\n" .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' ) . "\n"
-               );
-       }
-
-       /**
-        * Format a link to a group description page
-        *
-        * @param $group string
-        * @return string
-        */
-       private static function buildGroupLink( $group ) {
-               static $cache = array();
-               if( !isset( $cache[$group] ) )
-                       $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupName( $group ) );
-               return $cache[$group];
-       }
-       
-       /**
-        * Returns an array of all groups that may be edited
-        * @return array Array of groups that may be edited.
-        */
-        protected static function getAllGroups() {
-               return User::getAllGroups();
-        }
-
-       /**
-        * Adds a table with checkboxes where you can select what groups to add/remove
-        *
-        * @param $usergroups Array: groups the user belongs to
-        * @return string XHTML table element with checkboxes
-        */
-       private function groupCheckboxes( $usergroups ) {
-               $allgroups = $this->getAllGroups();
-               $ret = '';
-
-               $column = 1;
-               $settable_col = '';
-               $unsettable_col = '';
-
-               foreach ($allgroups as $group) {
-                       $set = in_array( $group, $usergroups );
-                       # Should the checkbox be disabled?
-                       $disabled = !(
-                               ( $set && $this->canRemove( $group ) ) ||
-                               ( !$set && $this->canAdd( $group ) ) );
-                       # Do we need to point out that this action is irreversible?
-                       $irreversible = !$disabled && (
-                               ($set && !$this->canAdd( $group )) ||
-                               (!$set && !$this->canRemove( $group ) ) );
-
-                       $attr = $disabled ? array( 'disabled' => 'disabled' ) : array();
-                       $text = $irreversible
-                               ? wfMsgHtml( 'userrights-irreversible-marker', User::getGroupMember( $group ) )
-                               : User::getGroupMember( $group );
-                       $checkbox = Xml::checkLabel( $text, "wpGroup-$group",
-                               "wpGroup-$group", $set, $attr );
-                       $checkbox = $disabled ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkbox ) : $checkbox;
-
-                       if ($disabled) {
-                               $unsettable_col .= "$checkbox<br />\n";
-                       } else {
-                               $settable_col .= "$checkbox<br />\n";
-                       }
-               }
-
-               if ($column) {
-                       $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) .
-                               "<tr>
-";
-                       if( $settable_col !== '' ) {
-                               $ret .= xml::element( 'th', null, wfMsg( 'userrights-changeable-col' ) );
-                       }
-                       if( $unsettable_col !== '' ) {
-                               $ret .= xml::element( 'th', null, wfMsg( 'userrights-unchangeable-col' ) );
-                       }
-                       $ret.= "</tr>
-                               <tr>
-";
-                       if( $settable_col !== '' ) {
-                               $ret .=
-"                                      <td style='vertical-align:top;'>
-                                               $settable_col
-                                       </td>
-";
-                       }
-                       if( $unsettable_col !== '' ) {
-                               $ret .=
-"                                      <td style='vertical-align:top;'>
-                                               $unsettable_col
-                                       </td>
-";
-                       }
-                       $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
-               }
-
-               return $ret;
-       }
-
-       /**
-        * @param  $group String: the name of the group to check
-        * @return bool Can we remove the group?
-        */
-       private function canRemove( $group ) {
-               // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
-               // PHP.
-               $groups = $this->changeableGroups();
-               return in_array( $group, $groups['remove'] ) || ($this->isself && in_array( $group, $groups['remove-self'] ));
-       }
-
-       /**
-        * @param $group string: the name of the group to check
-        * @return bool Can we add the group?
-        */
-       private function canAdd( $group ) {
-               $groups = $this->changeableGroups();
-               return in_array( $group, $groups['add'] ) || ($this->isself && in_array( $group, $groups['add-self'] ));
-       }
-
-       /**
-        * Returns an array of the groups that the user can add/remove.
-        *
-        * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
-        */
-       function changeableGroups() {
-               global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
-
-               if( $wgUser->isAllowed( 'userrights' ) ) {
-                       // This group gives the right to modify everything (reverse-
-                       // compatibility with old "userrights lets you change
-                       // everything")
-                       // Using array_merge to make the groups reindexed
-                       $all = array_merge( User::getAllGroups() );
-                       return array(
-                               'add' => $all,
-                               'remove' => $all,
-                               'add-self' => array(),
-                               'remove-self' => array()
-                       );
-               }
-
-               // Okay, it's not so simple, we will have to go through the arrays
-               $groups = array(
-                               'add' => array(),
-                               'remove' => array(),
-                               'add-self' => $wgGroupsAddToSelf,
-                               'remove-self' => $wgGroupsRemoveFromSelf);
-               $addergroups = $wgUser->getEffectiveGroups();
-
-               foreach ($addergroups as $addergroup) {
-                       $groups = array_merge_recursive(
-                               $groups, $this->changeableByGroup($addergroup)
-                       );
-                       $groups['add']    = array_unique( $groups['add'] );
-                       $groups['remove'] = array_unique( $groups['remove'] );
-               }
-               return $groups;
-       }
-
-       /**
-        * Returns an array of the groups that a particular group can add/remove.
-        *
-        * @param $group String: the group to check for whether it can add/remove
-        * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
-        */
-       private function changeableByGroup( $group ) {
-               global $wgAddGroups, $wgRemoveGroups;
-
-               $groups = array( 'add' => array(), 'remove' => array() );
-               if( empty($wgAddGroups[$group]) ) {
-                       // Don't add anything to $groups
-               } elseif( $wgAddGroups[$group] === true ) {
-                       // You get everything
-                       $groups['add'] = User::getAllGroups();
-               } elseif( is_array($wgAddGroups[$group]) ) {
-                       $groups['add'] = $wgAddGroups[$group];
-               }
-
-               // Same thing for remove
-               if( empty($wgRemoveGroups[$group]) ) {
-               } elseif($wgRemoveGroups[$group] === true ) {
-                       $groups['remove'] = User::getAllGroups();
-               } elseif( is_array($wgRemoveGroups[$group]) ) {
-                       $groups['remove'] = $wgRemoveGroups[$group];
-               }
-               return $groups;
-       }
-
-       /**
-        * Show a rights log fragment for the specified user
-        *
-        * @param $user User to show log for
-        * @param $output OutputPage to use
-        */
-       protected function showLogFragment( $user, $output ) {
-               $output->addHtml( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
-               LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage()->getPrefixedText() );
-       }
-}
diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php
deleted file mode 100644 (file)
index 8771fa1..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-<?php
-/**#@+
- * Give information about the version of MediaWiki, PHP, the DB and extensions
- *
- * @file
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-
-/**
- * constructor
- */
-function wfSpecialVersion() {
-       $version = new SpecialVersion;
-       $version->execute();
-}
-
-/**
- * @ingroup SpecialPage
- */
-class SpecialVersion {
-       private $firstExtOpened = true;
-
-       /**
-        * main()
-        */
-       function execute() {
-               global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks;
-               $wgMessageCache->loadAllMessages();
-
-               $wgOut->addHTML( '<div dir="ltr">' );
-               $text = 
-                       $this->MediaWikiCredits() .
-                       $this->softwareInformation() .
-                       $this->extensionCredits();
-               if  ( $wgSpecialVersionShowHooks ) {
-                       $text .= $this->wgHooks();
-               }
-               $wgOut->addWikiText( $text );
-               $wgOut->addHTML( $this->IPInfo() );
-               $wgOut->addHTML( '</div>' );
-       }
-
-       /**#@+
-        * @private
-        */
-
-       /**
-        * @return wiki text showing the license information
-        */
-       static function MediaWikiCredits() {
-               $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
-               "__NOTOC__
-               This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
-               copyright (C) 2001-2008 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
-               Tim Starling, Erik Möller, Gabriel Wicke, Ã†var Arnfjörð Bjarmason,
-               Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan and others.
-
-               MediaWiki is free software; you can redistribute it and/or modify
-               it under the terms of the GNU General Public License as published by
-               the Free Software Foundation; either version 2 of the License, or
-               (at your option) any later version.
-
-               MediaWiki is distributed in the hope that it will be useful,
-               but WITHOUT ANY WARRANTY; without even the implied warranty of
-               MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-               GNU General Public License for more details.
-
-               You should have received [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
-               along with this program; if not, write to the Free Software
-               Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-               or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
-               ";
-
-               return str_replace( "\t\t", '', $ret ) . "\n";
-       }
-
-       /**
-        * @return wiki text showing the third party software versions (apache, php, mysql).
-        */
-       static function softwareInformation() {
-               $dbr = wfGetDB( DB_SLAVE );
-
-               return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
-                       Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) .
-                               "<tr>
-                                       <th>" . wfMsg( 'version-software-product' ) . "</th>
-                                       <th>" . wfMsg( 'version-software-version' ) . "</th>
-                               </tr>\n
-                               <tr>
-                                       <td>[http://www.mediawiki.org/ MediaWiki]</td>
-                                       <td>" . self::getVersionLinked() . "</td>
-                               </tr>\n
-                               <tr>
-                                       <td>[http://www.php.net/ PHP]</td>
-                                       <td>" . phpversion() . " (" . php_sapi_name() . ")</td>
-                               </tr>\n
-                               <tr>
-                                       <td>" . $dbr->getSoftwareLink() . "</td>
-                                       <td>" . $dbr->getServerVersion() . "</td>
-                               </tr>\n" .
-                       Xml::closeElement( 'table' );
-       }
-
-       /**
-        * Return a string of the MediaWiki version with SVN revision if available
-        *
-        * @return mixed
-        */
-       public static function getVersion() {
-               global $wgVersion, $IP;
-               wfProfileIn( __METHOD__ );
-               $svn = self::getSvnRevision( $IP );
-               $version = $svn ? "$wgVersion (r$svn)" : $wgVersion;
-               wfProfileOut( __METHOD__ );
-               return $version;
-       }
-       
-       /**
-        * Return a string of the MediaWiki version with a link to SVN revision if
-        * available
-        *
-        * @return mixed
-        */
-       public static function getVersionLinked() {
-               global $wgVersion, $IP;
-               wfProfileIn( __METHOD__ );
-               $svn = self::getSvnRevision( $IP );
-               $viewvc = 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/?pathrev=';
-               $version = $svn ? "$wgVersion ([{$viewvc}{$svn} r$svn])" : $wgVersion;
-               wfProfileOut( __METHOD__ );
-               return $version;
-       }
-
-       /** Generate wikitext showing extensions name, URL, author and description */
-       function extensionCredits() {
-               global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
-
-               if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) )
-                       return '';
-
-               $extensionTypes = array(
-                       'specialpage' => wfMsg( 'version-specialpages' ),
-                       'parserhook' => wfMsg( 'version-parserhooks' ),
-                       'variable' => wfMsg( 'version-variables' ),
-                       'media' => wfMsg( 'version-mediahandlers' ),
-                       'other' => wfMsg( 'version-other' ),
-               );
-               wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
-
-               $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
-                       Xml::openElement( 'table', array( 'id' => 'sv-ext' ) );
-
-               foreach ( $extensionTypes as $type => $text ) {
-                       if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
-                               $out .= $this->openExtType( $text );
-
-                               usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
-
-                               foreach ( $wgExtensionCredits[$type] as $extension ) {
-                                       if ( isset( $extension['version'] ) ) {
-                                               $version = $extension['version'];
-                                       } elseif ( isset( $extension['svn-revision'] ) && 
-                                               preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/', 
-                                                       $extension['svn-revision'], $m ) ) 
-                                       {
-                                               $version = 'r' . $m[1];
-                                       } else {
-                                               $version = null;
-                                       }
-
-                                       $out .= $this->formatCredits(
-                                               isset ( $extension['name'] )           ? $extension['name']        : '',
-                                               $version,
-                                               isset ( $extension['author'] )         ? $extension['author']      : '',
-                                               isset ( $extension['url'] )            ? $extension['url']         : null,
-                                               isset ( $extension['description'] )    ? $extension['description'] : '',
-                                               isset ( $extension['descriptionmsg'] ) ? $extension['descriptionmsg'] : ''
-                                       );
-                               }
-                       }
-               }
-
-               if ( count( $wgExtensionFunctions ) ) {
-                       $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) );
-                       $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
-               }
-
-               if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
-                       for ( $i = 0; $i < $cnt; ++$i )
-                               $tags[$i] = "&lt;{$tags[$i]}&gt;";
-                       $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) );
-                       $out .= '<tr><td colspan="3">' . $this->listToText( $tags ). "</td></tr>\n";
-               }
-
-               if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) {
-                       $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) );
-                       $out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n";
-               }
-
-               if ( count( $wgSkinExtensionFunctions ) ) {
-                       $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) );
-                       $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
-               }
-               $out .= Xml::closeElement( 'table' );
-               return $out;
-       }
-
-       /** Callback to sort extensions by type */
-       function compare( $a, $b ) {
-               global $wgLang;
-               if( $a['name'] === $b['name'] ) {
-                       return 0;
-               } else {
-                       return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
-                               ? 1
-                               : -1;
-               }
-       }
-
-       function formatCredits( $name, $version = null, $author = null, $url = null, $description = null, $descriptionMsg = null ) {
-               $extension = isset( $url ) ? "[$url $name]" : $name;
-               $version = isset( $version ) ? "(" . wfMsg( 'version-version' ) . " $version)" : '';
-
-               # Look for a localized description
-               if( isset( $descriptionMsg ) ) {
-                       $msg = wfMsg( $descriptionMsg );
-                       if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
-                               $description = $msg;
-                       }
-               }
-
-               return "<tr>
-                               <td><em>$extension $version</em></td>
-                               <td>$description</td>
-                               <td>" . $this->listToText( (array)$author ) . "</td>
-                       </tr>\n";
-       }
-
-       /**
-        * @return string
-        */
-       function wgHooks() {
-               global $wgHooks;
-
-               if ( count( $wgHooks ) ) {
-                       $myWgHooks = $wgHooks;
-                       ksort( $myWgHooks );
-
-                       $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
-                               Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) .
-                               "<tr>
-                                       <th>" . wfMsg( 'version-hook-name' ) . "</th>
-                                       <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
-                               </tr>\n";
-
-                       foreach ( $myWgHooks as $hook => $hooks )
-                               $ret .= "<tr>
-                                               <td>$hook</td>
-                                               <td>" . $this->listToText( $hooks ) . "</td>
-                                       </tr>\n";
-
-                       $ret .= Xml::closeElement( 'table' );
-                       return $ret;
-               } else
-                       return '';
-       }
-
-       private function openExtType($text, $name = null) {
-               $opt = array( 'colspan' => 3 );
-               $out = '';
-
-               if(!$this->firstExtOpened) {
-                       // Insert a spacing line
-                       $out .= '<tr class="sv-space">' . Xml::element( 'td', $opt ) . "</tr>\n";
-               }
-               $this->firstExtOpened = false;
-
-               if($name) { $opt['id'] = "sv-$name"; }
-
-               $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n";
-               return $out;
-       }
-
-       /**
-        * @static
-        *
-        * @return string
-        */
-       function IPInfo() {
-               $ip =  str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
-               return "<!-- visited from $ip -->\n" .
-                       "<span style='display:none'>visited from $ip</span>";
-       }
-
-       /**
-        * @param array $list
-        * @return string
-        */
-       function listToText( $list ) {
-               $cnt = count( $list );
-
-               if ( $cnt == 1 ) {
-                       // Enforce always returning a string
-                       return (string)$this->arrayToString( $list[0] );
-               } elseif ( $cnt == 0 ) {
-                       return '';
-               } else {
-                       sort( $list );
-                       $t = array_slice( $list, 0, $cnt - 1 );
-                       $one = array_map( array( &$this, 'arrayToString' ), $t );
-                       $two = $this->arrayToString( $list[$cnt - 1] );
-                       $and = wfMsg( 'and' );
-
-                       return implode( ', ', $one ) . " $and $two";
-               }
-       }
-
-       /**
-        * @static
-        *
-        * @param mixed $list Will convert an array to string if given and return
-        *                    the paramater unaltered otherwise
-        * @return mixed
-        */
-       function arrayToString( $list ) {
-               if( is_object( $list ) ) {
-                       $class = get_class( $list );
-                       return "($class)";
-               } elseif ( ! is_array( $list ) ) {
-                       return $list;
-               } else {
-                       $class = get_class( $list[0] );
-                       return "($class, {$list[1]})";
-               }
-       }
-
-       /**
-        * Retrieve the revision number of a Subversion working directory.
-        *
-        * @param string $dir
-        * @return mixed revision number as int, or false if not a SVN checkout
-        */
-       public static function getSvnRevision( $dir ) {
-               // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
-               $entries = $dir . '/.svn/entries';
-
-               if( !file_exists( $entries ) ) {
-                       return false;
-               }
-
-               $content = file( $entries );
-
-               // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
-               if( preg_match( '/^<\?xml/', $content[0] ) ) {
-                       // subversion is release <= 1.3
-                       if( !function_exists( 'simplexml_load_file' ) ) {
-                               // We could fall back to expat... YUCK
-                               return false;
-                       }
-
-                       // SimpleXml whines about the xmlns...
-                       wfSuppressWarnings();
-                       $xml = simplexml_load_file( $entries );
-                       wfRestoreWarnings();
-
-                       if( $xml ) {
-                               foreach( $xml->entry as $entry ) {
-                                       if( $xml->entry[0]['name'] == '' ) {
-                                               // The directory entry should always have a revision marker.
-                                               if( $entry['revision'] ) {
-                                                       return intval( $entry['revision'] );
-                                               }
-                                       }
-                               }
-                       }
-                       return false;
-               } else {
-                       // subversion is release 1.4
-                       return intval( $content[3] );
-               }
-       }
-
-       /**#@-*/
-}
-
-/**#@-*/
diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php
deleted file mode 100644 (file)
index 969a8d8..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * A querypage to list the most wanted categories - implements Special:Wantedcategories
- *
- * @ingroup SpecialPage
- *
- * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
- * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
- */
-class WantedCategoriesPage extends QueryPage {
-
-       function getName() {
-               return 'Wantedcategories';
-       }
-
-       function isExpensive() {
-               return true;
-       }
-
-       function isSyndicated() {
-               return false;
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
-               $name = $dbr->addQuotes( $this->getName() );
-               return
-                       "
-                       SELECT
-                               $name as type,
-                               " . NS_CATEGORY . " as namespace,
-                               cl_to as title,
-                               COUNT(*) as value
-                       FROM $categorylinks
-                       LEFT JOIN $page ON cl_to = page_title AND page_namespace = ". NS_CATEGORY ."
-                       WHERE page_title IS NULL
-                       GROUP BY 1,2,3
-                       ";
-       }
-
-       function sortDescending() { return true; }
-
-       /**
-        * Fetch user page links and cache their existence
-        */
-       function preprocessResults( $db, $res ) {
-               $batch = new LinkBatch;
-               while ( $row = $db->fetchObject( $res ) )
-                       $batch->add( $row->namespace, $row->title );
-               $batch->execute();
-
-               // Back to start for display
-               if ( $db->numRows( $res ) > 0 )
-                       // If there are no rows we get an error seeking.
-                       $db->dataSeek( $res, 0 );
-       }
-
-       function formatResult( $skin, $result ) {
-               global $wgLang, $wgContLang;
-
-               $nt = Title::makeTitle( $result->namespace, $result->title );
-               $text = $wgContLang->convert( $nt->getText() );
-
-               $plink = $this->isCached() ?
-                       $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
-                       $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
-
-               $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
-                       $wgLang->formatNum( $result->value ) );
-               return wfSpecialList($plink, $nlinks);
-       }
-}
-
-/**
- * constructor
- */
-function wfSpecialWantedCategories() {
-       list( $limit, $offset ) = wfCheckLimits();
-
-       $wpp = new WantedCategoriesPage();
-
-       $wpp->doQuery( $offset, $limit );
-}
diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php
deleted file mode 100644 (file)
index 650e04f..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * implements Special:Wantedpages
- * @ingroup SpecialPage
- */
-class WantedPagesPage extends QueryPage {
-       var $nlinks;
-
-       function WantedPagesPage( $inc = false, $nlinks = true ) {
-               $this->setListoutput( $inc );
-               $this->nlinks = $nlinks;
-       }
-
-       function getName() {
-               return 'Wantedpages';
-       }
-
-       function isExpensive() {
-               return true;
-       }
-       function isSyndicated() { return false; }
-
-       function getSQL() {
-               global $wgWantedPagesThreshold;
-               $count = $wgWantedPagesThreshold - 1;
-               $dbr = wfGetDB( DB_SLAVE );
-               $pagelinks = $dbr->tableName( 'pagelinks' );
-               $page      = $dbr->tableName( 'page' );
-               return
-                       "SELECT 'Wantedpages' AS type,
-                               pl_namespace AS namespace,
-                               pl_title AS title,
-                               COUNT(*) AS value
-                        FROM $pagelinks
-                        LEFT JOIN $page AS pg1
-                        ON pl_namespace = pg1.page_namespace AND pl_title = pg1.page_title
-                        LEFT JOIN $page AS pg2
-                        ON pl_from = pg2.page_id
-                        WHERE pg1.page_namespace IS NULL
-                        AND pl_namespace NOT IN ( 2, 3 )
-                        AND pg2.page_namespace != 8
-                        GROUP BY 1,2,3
-                        HAVING COUNT(*) > $count";
-       }
-
-       /**
-        * Cache page existence for performance
-        */
-       function preprocessResults( $db, $res ) {
-               $batch = new LinkBatch;
-               while ( $row = $db->fetchObject( $res ) )
-                       $batch->add( $row->namespace, $row->title );
-               $batch->execute();
-
-               // Back to start for display
-               if ( $db->numRows( $res ) > 0 )
-                       // If there are no rows we get an error seeking.
-                       $db->dataSeek( $res, 0 );
-       }
-
-       /**
-        * Format an individual result
-        *
-        * @param $skin Skin to use for UI elements
-        * @param $result Result row
-        * @return string
-        */
-       public function formatResult( $skin, $result ) {
-               $title = Title::makeTitleSafe( $result->namespace, $result->title );
-               if( $title instanceof Title ) {
-                       if( $this->isCached() ) {
-                               $pageLink = $title->exists()
-                                       ? '<s>' . $skin->makeLinkObj( $title ) . '</s>'
-                                       : $skin->makeBrokenLinkObj( $title );
-                       } else {
-                               $pageLink = $skin->makeBrokenLinkObj( $title );
-                       }
-                       return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) );
-               } else {
-                       $tsafe = htmlspecialchars( $result->title );
-                       return "Invalid title in result set; {$tsafe}";
-               }
-       }
-
-       /**
-        * Make a "what links here" link for a specified result if required
-        *
-        * @param $title Title to make the link for
-        * @param $skin Skin to use
-        * @param $result Result row
-        * @return string
-        */
-       private function makeWlhLink( $title, $skin, $result ) {
-               global $wgLang;
-               if( $this->nlinks ) {
-                       $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
-                       $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
-                               $wgLang->formatNum( $result->value ) );
-                       return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
-               } else {
-                       return null;
-               }
-       }
-
-}
-
-/**
- * constructor
- */
-function wfSpecialWantedpages( $par = null, $specialPage ) {
-       $inc = $specialPage->including();
-
-       if ( $inc ) {
-               @list( $limit, $nlinks ) = explode( '/', $par, 2 );
-               $limit = (int)$limit;
-               $nlinks = $nlinks === 'nlinks';
-               $offset = 0;
-       } else {
-               list( $limit, $offset ) = wfCheckLimits();
-               $nlinks = true;
-       }
-
-       $wpp = new WantedPagesPage( $inc, $nlinks );
-
-       $wpp->doQuery( $offset, $limit, !$inc );
-}
diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php
deleted file mode 100644 (file)
index bb5e7ba..0000000
+++ /dev/null
@@ -1,377 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage Watchlist
- */
-
-/**
- *
- */
-require_once( dirname(__FILE__) . '/SpecialRecentchanges.php' );
-
-/**
- * Constructor
- *
- * @param $par Parameter passed to the page
- */
-function wfSpecialWatchlist( $par ) {
-       global $wgUser, $wgOut, $wgLang, $wgRequest;
-       global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
-       global $wgEnotifWatchlist;
-       $fname = 'wfSpecialWatchlist';
-
-       $skin = $wgUser->getSkin();
-       $specialTitle = SpecialPage::getTitleFor( 'Watchlist' );
-       $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
-       # Anons don't get a watchlist
-       if( $wgUser->isAnon() ) {
-               $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
-               $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
-               $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
-               return;
-       }
-
-       $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
-
-       $sub  = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
-       $sub .= '<br />' . WatchlistEditor::buildTools( $wgUser->getSkin() );
-       $wgOut->setSubtitle( $sub );
-
-       if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
-               $editor = new WatchlistEditor();
-               $editor->execute( $wgUser, $wgOut, $wgRequest, $mode );
-               return;
-       }
-
-       $uid = $wgUser->getId();
-       if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
-               $wgUser->clearAllNotifications( $uid );
-               $wgOut->redirect( $specialTitle->getFullUrl() );
-               return;
-       }
-
-       $defaults = array(
-       /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
-       /* bool  */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
-       /* bool  */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
-       /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
-       /* ?     */ 'namespace' => 'all',
-       );
-
-       extract($defaults);
-
-       # Extract variables from the request, falling back to user preferences or
-       # other default values if these don't exist
-       $prefs['days'    ] = floatval( $wgUser->getOption( 'watchlistdays' ) );
-       $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
-       $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
-       $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
-
-       # Get query variables
-       $days     = $wgRequest->getVal(  'days', $prefs['days'] );
-       $hideOwn  = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] );
-       $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] );
-       $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
-
-       # Get namespace value, if supplied, and prepare a WHERE fragment
-       $nameSpace = $wgRequest->getIntOrNull( 'namespace' );
-       if( !is_null( $nameSpace ) ) {
-               $nameSpace = intval( $nameSpace );
-               $nameSpaceClause = " AND rc_namespace = $nameSpace";
-       } else {
-               $nameSpace = '';
-               $nameSpaceClause = '';
-       }
-
-       $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
-       list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' );
-
-       $watchlistCount = $dbr->selectField( 'watchlist', 'COUNT(*)',
-               array( 'wl_user' => $uid ), __METHOD__ );
-       // Adjust for page X, talk:page X, which are both stored separately,
-       // but treated together
-       $nitems = floor($watchlistCount / 2);
-
-       if( is_null($days) || !is_numeric($days) ) {
-               $big = 1000; /* The magical big */
-               if($nitems > $big) {
-                       # Set default cutoff shorter
-                       $days = $defaults['days'] = (12.0 / 24.0); # 12 hours...
-               } else {
-                       $days = $defaults['days']; # default cutoff for shortlisters
-               }
-       } else {
-               $days = floatval($days);
-       }
-
-       // Dump everything here
-       $nondefaults = array();
-
-       wfAppendToArrayIfNotDefault('days'     , $days         , $defaults, $nondefaults);
-       wfAppendToArrayIfNotDefault('hideOwn'  , (int)$hideOwn , $defaults, $nondefaults);
-       wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $defaults, $nondefaults);
-       wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
-       wfAppendToArrayIfNotDefault('namespace', $nameSpace    , $defaults, $nondefaults);
-
-       $hookSql = "";
-       if( ! wfRunHooks('BeforeWatchlist', array($nondefaults, $wgUser, &$hookSql)) ) {
-               return;
-       }
-
-       if($nitems == 0) {
-               $wgOut->addWikiMsg( 'nowatchlist' );
-               return;
-       }
-
-       if ( $days <= 0 ) {
-               $andcutoff = '';
-       } else {
-               $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
-               /*
-               $sql = "SELECT COUNT(*) AS n FROM $page, $revision  WHERE rev_timestamp>'$cutoff' AND page_id=rev_page";
-               $res = $dbr->query( $sql, $fname );
-               $s = $dbr->fetchObject( $res );
-               $npages = $s->n;
-               */
-       }
-
-       # If the watchlist is relatively short, it's simplest to zip
-       # down its entirety and then sort the results.
-
-       # If it's relatively long, it may be worth our while to zip
-       # through the time-sorted page list checking for watched items.
-
-       # Up estimate of watched items by 15% to compensate for talk pages...
-
-       # Toggles
-       $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : '';
-       $andHideBots = $hideBots ? "AND (rc_bot = 0)" : '';
-       $andHideMinor = $hideMinor ? 'AND rc_minor = 0' : '';
-
-       # Show watchlist header
-       $header = '';
-       if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
-               $header .= wfMsg( 'wlheader-enotif' ) . "\n";
-       }
-       if ( $wgShowUpdatedMarker ) {
-               $header .= wfMsg( 'wlheader-showupdated' ) . "\n";
-       }
-
-  # Toggle watchlist content (all recent edits or just the latest)
-       if( $wgUser->getOption( 'extendwatchlist' )) {
-               $andLatest='';
-               $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
-       } else {
-       # Top log Ids for a page are not stored
-               $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
-               $limitWatchlist = '';
-       }
-
-       $header .= wfMsgExt( 'watchlist-details', array( 'parsemag' ), $wgLang->formatNum( $nitems ) );
-       $wgOut->addWikiText( $header );
-
-       # Show a message about slave lag, if applicable
-       if( ( $lag = $dbr->getLag() ) > 0 )
-               $wgOut->showLagWarning( $lag );
-
-       if ( $wgShowUpdatedMarker ) {
-               $wgOut->addHTML( '<form action="' .
-                       $specialTitle->escapeLocalUrl() .
-                       '" method="post"><input type="submit" name="dummy" value="' .
-                       htmlspecialchars( wfMsg( 'enotif_reset' ) ) .
-                       '" /><input type="hidden" name="reset" value="all" /></form>' .
-                       "\n\n" );
-       }
-       if ( $wgShowUpdatedMarker ) {
-               $wltsfield = ", ${watchlist}.wl_notificationtimestamp ";
-       } else {
-               $wltsfield = '';
-       }
-       $sql = "SELECT ${recentchanges}.* ${wltsfield}
-         FROM $watchlist,$recentchanges
-         LEFT JOIN $page ON rc_cur_id=page_id
-         WHERE wl_user=$uid
-         AND wl_namespace=rc_namespace
-         AND wl_title=rc_title
-         $andcutoff
-         $andLatest
-         $andHideOwn
-         $andHideBots
-         $andHideMinor
-         $nameSpaceClause
-         $hookSql
-         ORDER BY rc_timestamp DESC
-         $limitWatchlist";
-
-       $res = $dbr->query( $sql, $fname );
-       $numRows = $dbr->numRows( $res );
-
-       /* Start bottom header */
-       $wgOut->addHTML( "<hr />\n" );
-
-       if($days >= 1) {
-               $wgOut->addWikiText( wfMsgExt( 'rcnote', array( 'parseinline' ), $wgLang->formatNum( $numRows ),
-                       $wgLang->formatNum( $days ), $wgLang->timeAndDate( wfTimestampNow(), true ) ) . '<br />' , false );
-       } elseif($days > 0) {
-               $wgOut->addWikiText( wfMsgExt( 'wlnote', array( 'parseinline' ), $wgLang->formatNum( $numRows ),
-                       $wgLang->formatNum( round($days*24) ) ) . '<br />' , false );
-       }
-
-       $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" );
-
-       # Spit out some control panel links
-       $thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
-       $skin = $wgUser->getSkin();
-
-       # Hide/show bot edits
-       $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' );
-       $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
-       $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
-
-       # Hide/show own edits
-       $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' );
-       $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
-       $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
-
-       # Hide/show minor edits
-       $label = $hideMinor ? wfMsgHtml( 'watchlist-show-minor' ) : wfMsgHtml( 'watchlist-hide-minor' );
-       $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults );
-       $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
-
-       $wgOut->addHTML( implode( ' | ', $links ) );
-
-       # Form for namespace filtering
-       $form  = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) );
-       $form .= '<p>';
-       $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;';
-       $form .= Xml::namespaceSelector( $nameSpace, '' ) . '&nbsp;';
-       $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
-       $form .= Xml::hidden( 'days', $days );
-       if( $hideOwn )
-               $form .= Xml::hidden( 'hideOwn', 1 );
-       if( $hideBots )
-               $form .= Xml::hidden( 'hideBots', 1 );
-       if( $hideMinor )
-               $form .= Xml::hidden( 'hideMinor', 1 );
-       $form .= Xml::closeElement( 'form' );
-       $wgOut->addHtml( $form );
-
-       # If there's nothing to show, stop here
-       if( $numRows == 0 ) {
-               $wgOut->addWikiMsg( 'watchnochange' );
-               return;
-       }
-
-       /* End bottom header */
-
-       /* Do link batch query */
-       $linkBatch = new LinkBatch;
-       while ( $row = $dbr->fetchObject( $res ) ) {
-               $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
-               if ( $row->rc_user != 0 ) {
-                       $linkBatch->add( NS_USER, $userNameUnderscored );
-               }
-               $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
-       }
-       $linkBatch->execute();
-       $dbr->dataSeek( $res, 0 );
-
-       $list = ChangesList::newFromUser( $wgUser );
-
-       $s = $list->beginRecentChangesList();
-       $counter = 1;
-       while ( $obj = $dbr->fetchObject( $res ) ) {
-               # Make RC entry
-               $rc = RecentChange::newFromRow( $obj );
-               $rc->counter = $counter++;
-
-               if ( $wgShowUpdatedMarker ) {
-                       $updated = $obj->wl_notificationtimestamp;
-               } else {
-                       $updated = false;
-               }
-
-               if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
-                       $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
-                               'COUNT(*)',
-                               array(
-                                       'wl_namespace' => $obj->rc_namespace,
-                                       'wl_title' => $obj->rc_title,
-                               ),
-                               __METHOD__ );
-               } else {
-                       $rc->numberofWatchingusers = 0;
-               }
-
-               $s .= $list->recentChangesLine( $rc, $updated );
-       }
-       $s .= $list->endRecentChangesList();
-
-       $dbr->freeResult( $res );
-       $wgOut->addHTML( $s );
-
-}
-
-function wlHoursLink( $h, $page, $options = array() ) {
-       global $wgUser, $wgLang, $wgContLang;
-       $sk = $wgUser->getSkin();
-       $s = $sk->makeKnownLink(
-         $wgContLang->specialPage( $page ),
-         $wgLang->formatNum( $h ),
-         wfArrayToCGI( array('days' => ($h / 24.0)), $options ) );
-       return $s;
-}
-
-function wlDaysLink( $d, $page, $options = array() ) {
-       global $wgUser, $wgLang, $wgContLang;
-       $sk = $wgUser->getSkin();
-       $s = $sk->makeKnownLink(
-         $wgContLang->specialPage( $page ),
-         ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ),
-         wfArrayToCGI( array('days' => $d), $options ) );
-       return $s;
-}
-
-/**
- * Returns html
- */
-function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
-       $hours = array( 1, 2, 6, 12 );
-       $days = array( 1, 3, 7 );
-       $i = 0;
-       foreach( $hours as $h ) {
-               $hours[$i++] = wlHoursLink( $h, $page, $options );
-       }
-       $i = 0;
-       foreach( $days as $d ) {
-               $days[$i++] = wlDaysLink( $d, $page, $options );
-       }
-       return wfMsgExt('wlshowlast',
-               array('parseinline', 'replaceafter'),
-               implode(' | ', $hours),
-               implode(' | ', $days),
-               wlDaysLink( 0, $page, $options ) );
-}
-
-/**
- * Count the number of items on a user's watchlist
- *
- * @param $talk Include talk pages
- * @return integer
- */
-function wlCountItems( &$user, $talk = true ) {
-       $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
-
-       # Fetch the raw count
-       $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' );
-       $row = $dbr->fetchObject( $res );
-       $count = $row->count;
-       $dbr->freeResult( $res );
-
-       # Halve to remove talk pages if needed
-       if( !$talk )
-               $count = floor( $count / 2 );
-
-       return( $count );
-}
diff --git a/includes/SpecialWhatlinkshere.php b/includes/SpecialWhatlinkshere.php
deleted file mode 100644 (file)
index a57df5e..0000000
+++ /dev/null
@@ -1,408 +0,0 @@
-<?php
-/**
- * @todo Use some variant of Pager or something; the pagination here is lousy.
- *
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Entry point
- * @param $par String: An article name ??
- */
-function wfSpecialWhatlinkshere($par = NULL) {
-       global $wgRequest;
-       $page = new WhatLinksHerePage( $wgRequest, $par );
-       $page->execute();
-}
-
-/**
- * implements Special:Whatlinkshere
- * @ingroup SpecialPage
- */
-class WhatLinksHerePage {
-       // Stored data
-       protected $par;
-
-       // Stored objects
-       protected $opts, $target, $selfTitle;
-
-       // Stored globals
-       protected $skin, $request;
-
-       protected $limits = array( 20, 50, 100, 250, 500 );
-
-       function WhatLinksHerePage( $request, $par = null ) {
-               global $wgUser;
-               $this->request = $request;
-               $this->skin = $wgUser->getSkin();
-               $this->par = $par;
-       }
-
-       function execute() {
-               global $wgOut;
-
-               $opts = new FormOptions();
-
-               $opts->add( 'target', '' );
-               $opts->add( 'namespace', '', FormOptions::INTNULL );
-               $opts->add( 'limit', 50 );
-               $opts->add( 'from', 0 );
-               $opts->add( 'back', 0 );
-               $opts->add( 'hideredirs', false );
-               $opts->add( 'hidetrans', false );
-               $opts->add( 'hidelinks', false );
-               $opts->add( 'hideimages', false );
-
-               $opts->fetchValuesFromRequest( $this->request );
-               $opts->validateIntBounds( 'limit', 0, 5000 );
-
-               // Give precedence to subpage syntax
-               if ( isset($this->par) ) {
-                       $opts->setValue( 'target', $this->par );
-               }
-
-               // Bind to member variable
-               $this->opts = $opts;
-
-               $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
-               if( !$this->target ) {
-                       $wgOut->addHTML( $this->whatlinkshereForm() );
-                       return;
-               }
-
-               $this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() );
-
-               $wgOut->setPageTitle( wfMsgExt( 'whatlinkshere-title', 'escape', $this->target->getPrefixedText() ) );
-               $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) );
-
-               $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' '  .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
-
-               $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
-                       $opts->getValue( 'from' ), $opts->getValue( 'back' ) );
-       }
-
-       /**
-        * @param $level  int     Recursion level
-        * @param $target Title   Target title
-        * @param $limit  int     Number of entries to display
-        * @param $from   Title   Display from this article ID
-        * @param $back   Title   Display from this article ID at backwards scrolling
-        * @private
-        */
-       function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
-               global $wgOut, $wgMaxRedirectLinksRetrieved;
-               $dbr = wfGetDB( DB_SLAVE );
-               $options = array();
-
-               $hidelinks = $this->opts->getValue( 'hidelinks' );
-               $hideredirs = $this->opts->getValue( 'hideredirs' );
-               $hidetrans = $this->opts->getValue( 'hidetrans' );
-               $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' );
-
-               $fetchlinks = (!$hidelinks || !$hideredirs);
-
-               // Make the query
-               $plConds = array(
-                       'page_id=pl_from',
-                       'pl_namespace' => $target->getNamespace(),
-                       'pl_title' => $target->getDBkey(),
-               );
-               if( $hideredirs ) {
-                       $plConds['page_is_redirect'] = 0;
-               } elseif( $hidelinks ) {
-                       $plConds['page_is_redirect'] = 1;
-               }
-
-               $tlConds = array(
-                       'page_id=tl_from',
-                       'tl_namespace' => $target->getNamespace(),
-                       'tl_title' => $target->getDBkey(),
-               );
-
-               $ilConds = array(
-                       'page_id=il_from',
-                       'il_to' => $target->getDBkey(),
-               );
-
-               $namespace = $this->opts->getValue( 'namespace' );
-               if ( is_int($namespace) ) {
-                       $plConds['page_namespace'] = $namespace;
-                       $tlConds['page_namespace'] = $namespace;
-                       $ilConds['page_namespace'] = $namespace;
-               }
-
-               if ( $from ) {
-                       $tlConds[] = "tl_from >= $from";
-                       $plConds[] = "pl_from >= $from";
-                       $ilConds[] = "il_from >= $from";
-               }
-
-               // Read an extra row as an at-end check
-               $queryLimit = $limit + 1;
-
-               // Enforce join order, sometimes namespace selector may
-               // trigger filesorts which are far less efficient than scanning many entries
-               $options[] = 'STRAIGHT_JOIN';
-
-               $options['LIMIT'] = $queryLimit;
-               $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
-
-               if( $fetchlinks ) {
-                       $options['ORDER BY'] = 'pl_from';
-                       $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
-                               $plConds, __METHOD__, $options );
-               }
-
-               if( !$hidetrans ) {
-                       $options['ORDER BY'] = 'tl_from';
-                       $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
-                               $tlConds, __METHOD__, $options );
-               }
-
-               if( !$hideimages ) {
-                       $options['ORDER BY'] = 'il_from';
-                       $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields,
-                               $ilConds, __METHOD__, $options );
-               }
-
-               if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
-                       if ( 0 == $level ) {
-                               $wgOut->addHTML( $this->whatlinkshereForm() );
-                               $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
-                               $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
-                               // Show filters only if there are links
-                               if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
-                                       $wgOut->addHTML( $this->getFilterPanel() );
-                       }
-                       return;
-               }
-
-               // Read the rows into an array and remove duplicates
-               // templatelinks comes second so that the templatelinks row overwrites the
-               // pagelinks row, so we get (inclusion) rather than nothing
-               if( $fetchlinks ) {
-                       while ( $row = $dbr->fetchObject( $plRes ) ) {
-                               $row->is_template = 0;
-                               $row->is_image = 0;
-                               $rows[$row->page_id] = $row;
-                       }
-                       $dbr->freeResult( $plRes );
-
-               }
-               if( !$hidetrans ) {
-                       while ( $row = $dbr->fetchObject( $tlRes ) ) {
-                               $row->is_template = 1;
-                               $row->is_image = 0;
-                               $rows[$row->page_id] = $row;
-                       }
-                       $dbr->freeResult( $tlRes );
-               }
-               if( !$hideimages ) {
-                       while ( $row = $dbr->fetchObject( $ilRes ) ) {
-                               $row->is_template = 0;
-                               $row->is_image = 1;
-                               $rows[$row->page_id] = $row;
-                       }
-                       $dbr->freeResult( $ilRes );
-               }
-
-               // Sort by key and then change the keys to 0-based indices
-               ksort( $rows );
-               $rows = array_values( $rows );
-
-               $numRows = count( $rows );
-
-               // Work out the start and end IDs, for prev/next links
-               if ( $numRows > $limit ) {
-                       // More rows available after these ones
-                       // Get the ID from the last row in the result set
-                       $nextId = $rows[$limit]->page_id;
-                       // Remove undisplayed rows
-                       $rows = array_slice( $rows, 0, $limit );
-               } else {
-                       // No more rows after
-                       $nextId = false;
-               }
-               $prevId = $from;
-
-               if ( $level == 0 ) {
-                       $wgOut->addHTML( $this->whatlinkshereForm() );
-                       $wgOut->addHTML( $this->getFilterPanel() );
-                       $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
-
-                       $prevnext = $this->getPrevNext( $prevId, $nextId );
-                       $wgOut->addHTML( $prevnext );
-               }
-
-               $wgOut->addHTML( $this->listStart() );
-               foreach ( $rows as $row ) {
-                       $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
-
-                       if ( $row->page_is_redirect && $level < 2 ) {
-                               $wgOut->addHTML( $this->listItem( $row, $nt, true ) );
-                               $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved );
-                               $wgOut->addHTML( Xml::closeElement( 'li' ) );
-                       } else {
-                               $wgOut->addHTML( $this->listItem( $row, $nt ) );
-                       }
-               }
-
-               $wgOut->addHTML( $this->listEnd() );
-
-               if( $level == 0 ) {
-                       $wgOut->addHTML( $prevnext );
-               }
-       }
-
-       protected function listStart() {
-               return Xml::openElement( 'ul' );
-       }
-
-       protected function listItem( $row, $nt, $notClose = false ) {
-               # local message cache
-               static $msgcache = null;
-               if ( $msgcache === null ) {
-                       static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
-                               'whatlinkshere-links', 'isimage' );
-                       $msgcache = array();
-                       foreach ( $msgs as $msg ) {
-                               $msgcache[$msg] = wfMsgHtml( $msg );
-                       }
-               }
-
-               $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
-               $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
-
-               // Display properties (redirect or template)
-               $propsText = '';
-               $props = array();
-               if ( $row->page_is_redirect )
-                       $props[] = $msgcache['isredirect'];
-               if ( $row->is_template )
-                       $props[] = $msgcache['istemplate'];
-               if( $row->is_image )
-                       $props[] = $msgcache['isimage'];
-
-               if ( count( $props ) ) {
-                       $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
-               }
-
-               # Space for utilities links, with a what-links-here link provided
-               $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
-               $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
-
-               return $notClose ?
-                       Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
-                       Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
-       }
-
-       protected function listEnd() {
-               return Xml::closeElement( 'ul' );
-       }
-
-       protected function wlhLink( Title $target, $text ) {
-               static $title = null;
-               if ( $title === null )
-                       $title = SpecialPage::getTitleFor( 'Whatlinkshere' );
-
-               $targetText = $target->getPrefixedUrl();
-               return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
-       }
-
-       function makeSelfLink( $text, $query ) {
-               return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
-       }
-
-       function getPrevNext( $prevId, $nextId ) {
-               global $wgLang;
-               $currentLimit = $this->opts->getValue( 'limit' );
-               $fmtLimit = $wgLang->formatNum( $currentLimit );
-               $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit );
-               $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit );
-
-               $changed = $this->opts->getChangedValues();
-               unset($changed['target']); // Already in the request title
-
-               if ( 0 != $prevId ) {
-                       $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
-                       $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
-               }
-               if ( 0 != $nextId ) {
-                       $overrides = array( 'from' => $nextId, 'back' => $prevId );
-                       $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
-               }
-
-               $limitLinks = array();
-               foreach ( $this->limits as $limit ) {
-                       $prettyLimit = $wgLang->formatNum( $limit );
-                       $overrides = array( 'limit' => $limit );
-                       $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
-               }
-
-               $nums = implode ( ' | ', $limitLinks );
-
-               return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
-       }
-
-       function whatlinkshereForm() {
-               global $wgScript, $wgTitle;
-
-               // We get nicer value from the title object
-               $this->opts->consumeValue( 'target' );
-               // Reset these for new requests
-               $this->opts->consumeValues( array( 'back', 'from' ) );
-
-               $target = $this->target ? $this->target->getPrefixedText() : '';
-               $namespace = $this->opts->consumeValue( 'namespace' );
-
-               # Build up the form
-               $f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
-               
-               # Values that should not be forgotten
-               $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
-               foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
-                       $f .= Xml::hidden( $name, $value );
-               }
-
-               $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
-
-               # Target input
-               $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
-                               'mw-whatlinkshere-target', 40, $target );
-
-               $f .= ' ';
-
-               # Namespace selector
-               $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;' .
-                       Xml::namespaceSelector( $namespace, '' );
-
-               # Submit
-               $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
-
-               # Close
-               $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
-
-               return $f;
-       }
-
-       function getFilterPanel() {
-               $show = wfMsgHtml( 'show' );
-               $hide = wfMsgHtml( 'hide' );
-
-               $changed = $this->opts->getChangedValues();
-               unset($changed['target']); // Already in the request title
-
-               $links = array();
-               $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
-               if( $this->target->getNamespace() == NS_IMAGE )
-                       $types[] = 'hideimages';
-               foreach( $types as $type ) {
-                       $chosen = $this->opts->getValue( $type );
-                       $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
-                       $overrides = array( $type => !$chosen );
-                       $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
-               }
-               return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), implode( '&nbsp;|&nbsp;', $links ) );
-       }
-}
diff --git a/includes/SpecialWithoutinterwiki.php b/includes/SpecialWithoutinterwiki.php
deleted file mode 100644 (file)
index 2092e43..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-/**
- * @file
- * @ingroup SpecialPage
- */
-
-/**
- * Special page lists pages without language links
- *
- * @ingroup SpecialPage
- * @author Rob Church <robchur@gmail.com>
- */
-class WithoutInterwikiPage extends PageQueryPage {
-       private $prefix = '';
-
-       function getName() {
-               return 'Withoutinterwiki';
-       }
-
-       function getPageHeader() {
-               global $wgScript, $wgMiserMode;
-
-               # Do not show useless input form if wiki is running in misermode
-               if( $wgMiserMode ) {
-                       return '';
-               }
-
-               $prefix = $this->prefix;
-               $t = SpecialPage::getTitleFor( $this->getName() );
-
-               return  Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
-                       Xml::openElement( 'fieldset' ) .
-                       Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) .
-                       Xml::hidden( 'title', $t->getPrefixedText() ) .
-                       Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
-                       Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) .
-                       Xml::closeElement( 'fieldset' ) .
-                       Xml::closeElement( 'form' );
-       }
-
-       function sortDescending() {
-               return false;
-       }
-
-       function isExpensive() {
-               return true;
-       }
-
-       function isSyndicated() {
-               return false;
-       }
-
-       function getSQL() {
-               $dbr = wfGetDB( DB_SLAVE );
-               list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
-               $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
-               return
-                 "SELECT 'Withoutinterwiki'  AS type,
-                         page_namespace AS namespace,
-                         page_title     AS title,
-                         page_title     AS value
-                    FROM $page
-               LEFT JOIN $langlinks
-                      ON ll_from = page_id
-                   WHERE ll_title IS NULL
-                     AND page_namespace=" . NS_MAIN . "
-                     AND page_is_redirect = 0
-                         {$prefix}";
-       }
-
-       function setPrefix( $prefix = '' ) {
-               $this->prefix = $prefix;
-       }
-
-}
-
-function wfSpecialWithoutinterwiki() {
-       global $wgRequest, $wgContLang, $wgCapitalLinks;
-       list( $limit, $offset ) = wfCheckLimits();
-       if( $wgCapitalLinks ) {
-               $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) );
-       } else {
-               $prefix = $wgRequest->getVal( 'prefix' );
-       }
-       $wip = new WithoutInterwikiPage();
-       $wip->setPrefix( $prefix );
-       $wip->doQuery( $offset, $limit );
-}
index 2154eba..411c211 100644 (file)
@@ -69,29 +69,32 @@ define( 'MEDIAWIKI', true );
 # Makes it possible to for example to have effective exclude path in apc.
 # Also doesn't break installations using symlinked includes, like
 # dirname( __FILE__ ) would do.
-$preIP = realpath( '.' );
+$IP = getenv( 'MW_INSTALL_PATH' );
+if ( $IP === false ) {
+       $IP = realpath( '.' );
+}
 
 # Start profiler
-require_once( "$preIP/StartProfiler.php" );
+require_once( "$IP/StartProfiler.php" );
 wfProfileIn( 'WebStart.php-conf' );
 
 # Load up some global defines.
-require_once( "$preIP/includes/Defines.php" );
+require_once( "$IP/includes/Defines.php" );
 
 # LocalSettings.php is the per site customization file. If it does not exit
 # the wiki installer need to be launched or the generated file moved from
 # ./config/ to ./
-if( !file_exists( "$preIP/LocalSettings.php" ) ) {
-       # DefaultSettings assumes $IP is defined, like it usually is when included
-       # in LocalSettings. But in this case we need to provide the default one.
-       $IP = $preIP;
-       require_once( "$preIP/includes/DefaultSettings.php" ); # used for printing the version
-       require_once( "$preIP/includes/templates/NoLocalSettings.php" );
+if( !file_exists( "$IP/LocalSettings.php" ) ) {
+       require_once( "$IP/includes/DefaultSettings.php" ); # used for printing the version
+       require_once( "$IP/includes/templates/NoLocalSettings.php" );
        die();
 }
 
-# Include site settings. Most importantly, $IP should be available after this.
-require_once( "$preIP/LocalSettings.php" );
+# Start the autoloader, so that extensions can derive classes from core files
+require_once( "$IP/includes/AutoLoader.php" );
+
+# Include site settings. $IP may be changed (hopefully before the AutoLoader is invoked)
+require_once( "$IP/LocalSettings.php" );
 wfProfileOut( 'WebStart.php-conf' );
 
 wfProfileIn( 'WebStart.php-ob_start' );
diff --git a/includes/db/Database.php b/includes/db/Database.php
new file mode 100644 (file)
index 0000000..b6a8c8b
--- /dev/null
@@ -0,0 +1,2700 @@
+<?php
+/**
+ * @defgroup Database Database
+ *
+ * @file
+ * @ingroup Database
+ * This file deals with MySQL interface functions
+ * and query specifics/optimisations
+ */
+
+/** Number of times to re-try an operation in case of deadlock */
+define( 'DEADLOCK_TRIES', 4 );
+/** Minimum time to wait before retry, in microseconds */
+define( 'DEADLOCK_DELAY_MIN', 500000 );
+/** Maximum time to wait before retry */
+define( 'DEADLOCK_DELAY_MAX', 1500000 );
+
+/**
+ * Database abstraction object
+ * @ingroup Database
+ */
+class Database {
+
+#------------------------------------------------------------------------------
+# Variables
+#------------------------------------------------------------------------------
+
+       protected $mLastQuery = '';
+       protected $mPHPError = false;
+
+       protected $mServer, $mUser, $mPassword, $mConn = null, $mDBname;
+       protected $mOut, $mOpened = false;
+
+       protected $mFailFunction;
+       protected $mTablePrefix;
+       protected $mFlags;
+       protected $mTrxLevel = 0;
+       protected $mErrorCount = 0;
+       protected $mLBInfo = array();
+       protected $mFakeSlaveLag = null, $mFakeMaster = false;
+
+#------------------------------------------------------------------------------
+# Accessors
+#------------------------------------------------------------------------------
+       # These optionally set a variable and return the previous state
+
+       /**
+        * Fail function, takes a Database as a parameter
+        * Set to false for default, 1 for ignore errors
+        */
+       function failFunction( $function = NULL ) {
+               return wfSetVar( $this->mFailFunction, $function );
+       }
+
+       /**
+        * Output page, used for reporting errors
+        * FALSE means discard output
+        */
+       function setOutputPage( $out ) {
+               $this->mOut = $out;
+       }
+
+       /**
+        * Boolean, controls output of large amounts of debug information
+        */
+       function debug( $debug = NULL ) {
+               return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
+       }
+
+       /**
+        * Turns buffering of SQL result sets on (true) or off (false).
+        * Default is "on" and it should not be changed without good reasons.
+        */
+       function bufferResults( $buffer = NULL ) {
+               if ( is_null( $buffer ) ) {
+                       return !(bool)( $this->mFlags & DBO_NOBUFFER );
+               } else {
+                       return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
+               }
+       }
+
+       /**
+        * Turns on (false) or off (true) the automatic generation and sending
+        * of a "we're sorry, but there has been a database error" page on
+        * database errors. Default is on (false). When turned off, the
+        * code should use lastErrno() and lastError() to handle the
+        * situation as appropriate.
+        */
+       function ignoreErrors( $ignoreErrors = NULL ) {
+               return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
+       }
+
+       /**
+        * The current depth of nested transactions
+        * @param $level Integer: , default NULL.
+        */
+       function trxLevel( $level = NULL ) {
+               return wfSetVar( $this->mTrxLevel, $level );
+       }
+
+       /**
+        * Number of errors logged, only useful when errors are ignored
+        */
+       function errorCount( $count = NULL ) {
+               return wfSetVar( $this->mErrorCount, $count );
+       }
+
+       function tablePrefix( $prefix = null ) {
+               return wfSetVar( $this->mTablePrefix, $prefix );
+       }
+
+       /**
+        * Properties passed down from the server info array of the load balancer
+        */
+       function getLBInfo( $name = NULL ) {
+               if ( is_null( $name ) ) {
+                       return $this->mLBInfo;
+               } else {
+                       if ( array_key_exists( $name, $this->mLBInfo ) ) {
+                               return $this->mLBInfo[$name];
+                       } else {
+                               return NULL;
+                       }
+               }
+       }
+
+       function setLBInfo( $name, $value = NULL ) {
+               if ( is_null( $value ) ) {
+                       $this->mLBInfo = $name;
+               } else {
+                       $this->mLBInfo[$name] = $value;
+               }
+       }
+
+       /**
+        * Set lag time in seconds for a fake slave
+        */
+       function setFakeSlaveLag( $lag ) {
+               $this->mFakeSlaveLag = $lag;
+       }
+
+       /**
+        * Make this connection a fake master
+        */
+       function setFakeMaster( $enabled = true ) {
+               $this->mFakeMaster = $enabled;
+       }
+
+       /**
+        * Returns true if this database supports (and uses) cascading deletes
+        */
+       function cascadingDeletes() {
+               return false;
+       }
+
+       /**
+        * Returns true if this database supports (and uses) triggers (e.g. on the page table)
+        */
+       function cleanupTriggers() {
+               return false;
+       }
+
+       /**
+        * Returns true if this database is strict about what can be put into an IP field.
+        * Specifically, it uses a NULL value instead of an empty string.
+        */
+       function strictIPs() {
+               return false;
+       }
+
+       /**
+        * Returns true if this database uses timestamps rather than integers
+       */
+       function realTimestamps() {
+               return false;
+       }
+
+       /**
+        * Returns true if this database does an implicit sort when doing GROUP BY
+        */
+       function implicitGroupby() {
+               return true;
+       }
+
+       /**
+        * Returns true if this database does an implicit order by when the column has an index
+        * For example: SELECT page_title FROM page LIMIT 1
+        */
+       function implicitOrderby() {
+               return true;
+       }
+
+       /**
+        * Returns true if this database can do a native search on IP columns
+        * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
+        */
+       function searchableIPs() {
+               return false;
+       }
+
+       /**
+        * Returns true if this database can use functional indexes
+        */
+       function functionalIndexes() {
+               return false;
+       }
+
+       /**#@+
+        * Get function
+        */
+       function lastQuery() { return $this->mLastQuery; }
+       function isOpen() { return $this->mOpened; }
+       /**#@-*/
+
+       function setFlag( $flag ) {
+               $this->mFlags |= $flag;
+       }
+
+       function clearFlag( $flag ) {
+               $this->mFlags &= ~$flag;
+       }
+
+       function getFlag( $flag ) {
+               return !!($this->mFlags & $flag);
+       }
+
+       /**
+        * General read-only accessor
+        */
+       function getProperty( $name ) {
+               return $this->$name;
+       }
+
+       function getWikiID() {
+               if( $this->mTablePrefix ) {
+                       return "{$this->mDBname}-{$this->mTablePrefix}";
+               } else {
+                       return $this->mDBname;
+               }
+       }
+
+#------------------------------------------------------------------------------
+# Other functions
+#------------------------------------------------------------------------------
+
+       /**@{{
+        * Constructor.
+        * @param string $server database server host
+        * @param string $user database user name
+        * @param string $password database user password
+        * @param string $dbname database name
+        * @param failFunction
+        * @param $flags
+        * @param $tablePrefix String: database table prefixes. By default use the prefix gave in LocalSettings.php
+        */
+       function __construct( $server = false, $user = false, $password = false, $dbName = false,
+               $failFunction = false, $flags = 0, $tablePrefix = 'get from global' ) {
+
+               global $wgOut, $wgDBprefix, $wgCommandLineMode;
+               # Can't get a reference if it hasn't been set yet
+               if ( !isset( $wgOut ) ) {
+                       $wgOut = NULL;
+               }
+               $this->mOut =& $wgOut;
+
+               $this->mFailFunction = $failFunction;
+               $this->mFlags = $flags;
+
+               if ( $this->mFlags & DBO_DEFAULT ) {
+                       if ( $wgCommandLineMode ) {
+                               $this->mFlags &= ~DBO_TRX;
+                       } else {
+                               $this->mFlags |= DBO_TRX;
+                       }
+               }
+
+               /*
+               // Faster read-only access
+               if ( wfReadOnly() ) {
+                       $this->mFlags |= DBO_PERSISTENT;
+                       $this->mFlags &= ~DBO_TRX;
+               }*/
+
+               /** Get the default table prefix*/
+               if ( $tablePrefix == 'get from global' ) {
+                       $this->mTablePrefix = $wgDBprefix;
+               } else {
+                       $this->mTablePrefix = $tablePrefix;
+               }
+
+               if ( $server ) {
+                       $this->open( $server, $user, $password, $dbName );
+               }
+       }
+
+       /**
+        * @static
+        * @param failFunction
+        * @param $flags
+        */
+       static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
+       {
+               return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
+       }
+
+       /**
+        * Usually aborts on failure
+        * If the failFunction is set to a non-zero integer, returns success
+        */
+       function open( $server, $user, $password, $dbName ) {
+               global $wguname, $wgAllDBsAreLocalhost;
+               wfProfileIn( __METHOD__ );
+
+               # Test for missing mysql.so
+               # First try to load it
+               if (!@extension_loaded('mysql')) {
+                       @dl('mysql.so');
+               }
+
+               # Fail now
+               # Otherwise we get a suppressed fatal error, which is very hard to track down
+               if ( !function_exists( 'mysql_connect' ) ) {
+                       throw new DBConnectionError( $this, "MySQL functions missing, have you compiled PHP with the --with-mysql option?\n" );
+               }
+
+               # Debugging hack -- fake cluster
+               if ( $wgAllDBsAreLocalhost ) {
+                       $realServer = 'localhost';
+               } else {
+                       $realServer = $server;
+               }
+               $this->close();
+               $this->mServer = $server;
+               $this->mUser = $user;
+               $this->mPassword = $password;
+               $this->mDBname = $dbName;
+
+               $success = false;
+
+               wfProfileIn("dbconnect-$server");
+
+               # Try to connect up to three times
+               # The kernel's default SYN retransmission period is far too slow for us,
+               # so we use a short timeout plus a manual retry.
+               $this->mConn = false;
+               $max = 3;
+               $this->installErrorHandler();
+               for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+                       if ( $i > 1 ) {
+                               usleep( 1000 );
+                       }
+                       if ( $this->mFlags & DBO_PERSISTENT ) {
+                               $this->mConn = mysql_pconnect( $realServer, $user, $password );
+                       } else {
+                               # Create a new connection...
+                               $this->mConn = mysql_connect( $realServer, $user, $password, true );
+                       }
+                       if ($this->mConn === false) {
+                               #$iplus = $i + 1;
+                               #wfLogDBError("Connect loop error $iplus of $max ($server): " . mysql_errno() . " - " . mysql_error()."\n"); 
+                       }
+               }
+               $phpError = $this->restoreErrorHandler();
+               
+               wfProfileOut("dbconnect-$server");
+
+               if ( $dbName != '' ) {
+                       if ( $this->mConn !== false ) {
+                               $success = @/**/mysql_select_db( $dbName, $this->mConn );
+                               if ( !$success ) {
+                                       $error = "Error selecting database $dbName on server {$this->mServer} " .
+                                               "from client host {$wguname['nodename']}\n";
+                                       wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+                                       wfDebug( $error );
+                               }
+                       } else {
+                               wfDebug( "DB connection error\n" );
+                               wfDebug( "Server: $server, User: $user, Password: " .
+                                       substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
+                               $success = false;
+                       }
+               } else {
+                       # Delay USE query
+                       $success = (bool)$this->mConn;
+               }
+
+               if ( $success ) {
+                       $version = $this->getServerVersion();
+                       if ( version_compare( $version, '4.1' ) >= 0 ) {
+                               // Tell the server we're communicating with it in UTF-8.
+                               // This may engage various charset conversions.
+                               global $wgDBmysql5;
+                               if( $wgDBmysql5 ) {
+                                       $this->query( 'SET NAMES utf8', __METHOD__ );
+                               }
+                               // Turn off strict mode
+                               $this->query( "SET sql_mode = ''", __METHOD__ );
+                       }
+
+                       // Turn off strict mode if it is on
+               } else {
+                       $this->reportConnectionError( $phpError );
+               }
+
+               $this->mOpened = $success;
+               wfProfileOut( __METHOD__ );
+               return $success;
+       }
+       /**@}}*/
+
+       protected function installErrorHandler() {
+               $this->mPHPError = false;
+               set_error_handler( array( $this, 'connectionErrorHandler' ) );
+       }
+
+       protected function restoreErrorHandler() {
+               restore_error_handler();
+               return $this->mPHPError;
+       }
+
+       protected function connectionErrorHandler( $errno,  $errstr ) {
+               $this->mPHPError = $errstr;
+       }
+
+       /**
+        * Closes a database connection.
+        * if it is open : commits any open transactions
+        *
+        * @return bool operation success. true if already closed.
+        */
+       function close()
+       {
+               $this->mOpened = false;
+               if ( $this->mConn ) {
+                       if ( $this->trxLevel() ) {
+                               $this->immediateCommit();
+                       }
+                       return mysql_close( $this->mConn );
+               } else {
+                       return true;
+               }
+       }
+
+       /**
+        * @param string $error fallback error message, used if none is given by MySQL
+        */
+       function reportConnectionError( $error = 'Unknown error' ) {
+               $myError = $this->lastError();
+               if ( $myError ) {
+                       $error = $myError;
+               }
+
+               if ( $this->mFailFunction ) {
+                       # Legacy error handling method
+                       if ( !is_int( $this->mFailFunction ) ) {
+                               $ff = $this->mFailFunction;
+                               $ff( $this, $error );
+                       }
+               } else {
+                       # New method
+                       wfLogDBError( "Connection error: $error\n" );
+                       throw new DBConnectionError( $this, $error );
+               }
+       }
+
+       /**
+        * Usually aborts on failure.  If errors are explicitly ignored, returns success.
+        *
+        * @param  $sql        String: SQL query
+        * @param  $fname      String: Name of the calling function, for profiling/SHOW PROCESSLIST 
+        *     comment (you can use __METHOD__ or add some extra info)
+        * @param  $tempIgnore Bool:   Whether to avoid throwing an exception on errors... 
+        *     maybe best to catch the exception instead?
+        * @return true for a successful write query, ResultWrapper object for a successful read query, 
+        *     or false on failure if $tempIgnore set
+        * @throws DBQueryError Thrown when the database returns an error of any kind
+        */
+       public function query( $sql, $fname = '', $tempIgnore = false ) {
+               global $wgProfiler;
+
+               $isMaster = !is_null( $this->getLBInfo( 'master' ) );
+               if ( isset( $wgProfiler ) ) {
+                       # generalizeSQL will probably cut down the query to reasonable
+                       # logging size most of the time. The substr is really just a sanity check.
+
+                       # Who's been wasting my precious column space? -- TS
+                       #$profName = 'query: ' . $fname . ' ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+
+                       if ( $isMaster ) {
+                               $queryProf = 'query-m: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+                               $totalProf = 'Database::query-master';
+                       } else {
+                               $queryProf = 'query: ' . substr( Database::generalizeSQL( $sql ), 0, 255 );
+                               $totalProf = 'Database::query';
+                       }
+                       wfProfileIn( $totalProf );
+                       wfProfileIn( $queryProf );
+               }
+
+               $this->mLastQuery = $sql;
+
+               # Add a comment for easy SHOW PROCESSLIST interpretation
+               #if ( $fname ) {
+                       global $wgUser;
+                       if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
+                               $userName = $wgUser->getName();
+                               if ( mb_strlen( $userName ) > 15 ) {
+                                       $userName = mb_substr( $userName, 0, 15 ) . '...';
+                               }
+                               $userName = str_replace( '/', '', $userName );
+                       } else {
+                               $userName = '';
+                       }
+                       $commentedSql = preg_replace('/\s/', " /* $fname $userName */ ", $sql, 1);
+               #} else {
+               #       $commentedSql = $sql;
+               #}
+
+               # If DBO_TRX is set, start a transaction
+               if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && 
+                       $sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK') {
+                       // avoid establishing transactions for SHOW and SET statements too -
+                       // that would delay transaction initializations to once connection 
+                       // is really used by application
+                       $sqlstart = substr($sql,0,10); // very much worth it, benchmark certified(tm)
+                       if (strpos($sqlstart,"SHOW ")!==0 and strpos($sqlstart,"SET ")!==0) 
+                               $this->begin(); 
+               }
+
+               if ( $this->debug() ) {
+                       $sqlx = substr( $commentedSql, 0, 500 );
+                       $sqlx = strtr( $sqlx, "\t\n", '  ' );
+                       if ( $isMaster ) {
+                               wfDebug( "SQL-master: $sqlx\n" );
+                       } else {
+                               wfDebug( "SQL: $sqlx\n" );
+                       }
+               }
+
+               # Do the query and handle errors
+               $ret = $this->doQuery( $commentedSql );
+
+               # Try reconnecting if the connection was lost
+               if ( false === $ret && ( $this->lastErrno() == 2013 || $this->lastErrno() == 2006 ) ) {
+                       # Transaction is gone, like it or not
+                       $this->mTrxLevel = 0;
+                       wfDebug( "Connection lost, reconnecting...\n" );
+                       if ( $this->ping() ) {
+                               wfDebug( "Reconnected\n" );
+                               $sqlx = substr( $commentedSql, 0, 500 );
+                               $sqlx = strtr( $sqlx, "\t\n", '  ' );
+                               global $wgRequestTime;
+                               $elapsed = round( microtime(true) - $wgRequestTime, 3 );
+                               wfLogDBError( "Connection lost and reconnected after {$elapsed}s, query: $sqlx\n" );
+                               $ret = $this->doQuery( $commentedSql );
+                       } else {
+                               wfDebug( "Failed\n" );
+                       }
+               }
+
+               if ( false === $ret ) {
+                       $this->reportQueryError( $this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
+               }
+
+               if ( isset( $wgProfiler ) ) {
+                       wfProfileOut( $queryProf );
+                       wfProfileOut( $totalProf );
+               }
+               return $this->resultObject( $ret );
+       }
+
+       /**
+        * The DBMS-dependent part of query()
+        * @param  $sql String: SQL query.
+        * @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
+        * @access private
+        */
+       /*private*/ function doQuery( $sql ) {
+               if( $this->bufferResults() ) {
+                       $ret = mysql_query( $sql, $this->mConn );
+               } else {
+                       $ret = mysql_unbuffered_query( $sql, $this->mConn );
+               }
+               return $ret;
+       }
+
+       /**
+        * @param $error
+        * @param $errno
+        * @param $sql
+        * @param string $fname
+        * @param bool $tempIgnore
+        */
+       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+               global $wgCommandLineMode;
+               # Ignore errors during error handling to avoid infinite recursion
+               $ignore = $this->ignoreErrors( true );
+               ++$this->mErrorCount;
+
+               if( $ignore || $tempIgnore ) {
+                       wfDebug("SQL ERROR (ignored): $error\n");
+                       $this->ignoreErrors( $ignore );
+               } else {
+                       $sql1line = str_replace( "\n", "\\n", $sql );
+                       wfLogDBError("$fname\t{$this->mServer}\t$errno\t$error\t$sql1line\n");
+                       wfDebug("SQL ERROR: " . $error . "\n");
+                       throw new DBQueryError( $this, $error, $errno, $sql, $fname );
+               }
+       }
+
+
+       /**
+        * Intended to be compatible with the PEAR::DB wrapper functions.
+        * http://pear.php.net/manual/en/package.database.db.intro-execute.php
+        *
+        * ? = scalar value, quoted as necessary
+        * ! = raw SQL bit (a function for instance)
+        * & = filename; reads the file and inserts as a blob
+        *     (we don't use this though...)
+        */
+       function prepare( $sql, $func = 'Database::prepare' ) {
+               /* MySQL doesn't support prepared statements (yet), so just
+                  pack up the query for reference. We'll manually replace
+                  the bits later. */
+               return array( 'query' => $sql, 'func' => $func );
+       }
+
+       function freePrepared( $prepared ) {
+               /* No-op for MySQL */
+       }
+
+       /**
+        * Execute a prepared query with the various arguments
+        * @param string $prepared the prepared sql
+        * @param mixed $args Either an array here, or put scalars as varargs
+        */
+       function execute( $prepared, $args = null ) {
+               if( !is_array( $args ) ) {
+                       # Pull the var args
+                       $args = func_get_args();
+                       array_shift( $args );
+               }
+               $sql = $this->fillPrepared( $prepared['query'], $args );
+               return $this->query( $sql, $prepared['func'] );
+       }
+
+       /**
+        * Prepare & execute an SQL statement, quoting and inserting arguments
+        * in the appropriate places.
+        * @param string $query
+        * @param string $args ...
+        */
+       function safeQuery( $query, $args = null ) {
+               $prepared = $this->prepare( $query, 'Database::safeQuery' );
+               if( !is_array( $args ) ) {
+                       # Pull the var args
+                       $args = func_get_args();
+                       array_shift( $args );
+               }
+               $retval = $this->execute( $prepared, $args );
+               $this->freePrepared( $prepared );
+               return $retval;
+       }
+
+       /**
+        * For faking prepared SQL statements on DBs that don't support
+        * it directly.
+        * @param string $preparedSql - a 'preparable' SQL statement
+        * @param array $args - array of arguments to fill it with
+        * @return string executable SQL
+        */
+       function fillPrepared( $preparedQuery, $args ) {
+               reset( $args );
+               $this->preparedArgs =& $args;
+               return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
+                       array( &$this, 'fillPreparedArg' ), $preparedQuery );
+       }
+
+       /**
+        * preg_callback func for fillPrepared()
+        * The arguments should be in $this->preparedArgs and must not be touched
+        * while we're doing this.
+        *
+        * @param array $matches
+        * @return string
+        * @private
+        */
+       function fillPreparedArg( $matches ) {
+               switch( $matches[1] ) {
+                       case '\\?': return '?';
+                       case '\\!': return '!';
+                       case '\\&': return '&';
+               }
+               list( /* $n */ , $arg ) = each( $this->preparedArgs );
+               switch( $matches[1] ) {
+                       case '?': return $this->addQuotes( $arg );
+                       case '!': return $arg;
+                       case '&':
+                               # return $this->addQuotes( file_get_contents( $arg ) );
+                               throw new DBUnexpectedError( $this, '& mode is not implemented. If it\'s really needed, uncomment the line above.' );
+                       default:
+                               throw new DBUnexpectedError( $this, 'Received invalid match. This should never happen!' );
+               }
+       }
+
+       /**#@+
+        * @param mixed $res A SQL result
+        */
+       /**
+        * Free a result object
+        */
+       function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               if ( !@/**/mysql_free_result( $res ) ) {
+                       throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+               }
+       }
+
+       /**
+        * Fetch the next row from the given result object, in object form.
+        * Fields can be retrieved with $row->fieldname, with fields acting like
+        * member variables.
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
+        */
+       function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @/**/$row = mysql_fetch_object( $res );
+               if( $this->lastErrno() ) {
+                       throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+               }
+               return $row;
+       }
+
+       /**
+        * Fetch the next row from the given result object, in associative array
+        * form.  Fields are retrieved with $row['fieldname'].
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
+        */
+       function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @/**/$row = mysql_fetch_array( $res );
+               if ( $this->lastErrno() ) {
+                       throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+               }
+               return $row;
+       }
+
+       /**
+        * Get the number of rows in a result object
+        */
+       function numRows( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @/**/$n = mysql_num_rows( $res );
+               if( $this->lastErrno() ) {
+                       throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+               }
+               return $n;
+       }
+
+       /**
+        * Get the number of fields in a result object
+        * See documentation for mysql_num_fields()
+        */
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_num_fields( $res );
+       }
+
+       /**
+        * Get a field name in a result object
+        * See documentation for mysql_field_name():
+        * http://www.php.net/mysql_field_name
+        */
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_field_name( $res, $n );
+       }
+
+       /**
+        * Get the inserted value of an auto-increment row
+        *
+        * The value inserted should be fetched from nextSequenceValue()
+        *
+        * Example:
+        * $id = $dbw->nextSequenceValue('page_page_id_seq');
+        * $dbw->insert('page',array('page_id' => $id));
+        * $id = $dbw->insertId();
+        */
+       function insertId() { return mysql_insert_id( $this->mConn ); }
+
+       /**
+        * Change the position of the cursor in a result object
+        * See mysql_data_seek()
+        */
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_data_seek( $res, $row );
+       }
+
+       /**
+        * Get the last error number
+        * See mysql_errno()
+        */
+       function lastErrno() {
+               if ( $this->mConn ) {
+                       return mysql_errno( $this->mConn );
+               } else {
+                       return mysql_errno();
+               }
+       }
+
+       /**
+        * Get a description of the last error
+        * See mysql_error() for more details
+        */
+       function lastError() {
+               if ( $this->mConn ) {
+                       # Even if it's non-zero, it can still be invalid
+                       wfSuppressWarnings();
+                       $error = mysql_error( $this->mConn );
+                       if ( !$error ) {
+                               $error = mysql_error();
+                       }
+                       wfRestoreWarnings();
+               } else {
+                       $error = mysql_error();
+               }
+               if( $error ) {
+                       $error .= ' (' . $this->mServer . ')';
+               }
+               return $error;
+       }
+       /**
+        * Get the number of rows affected by the last write query
+        * See mysql_affected_rows() for more details
+        */
+       function affectedRows() { return mysql_affected_rows( $this->mConn ); }
+       /**#@-*/ // end of template : @param $result
+
+       /**
+        * Simple UPDATE wrapper
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns success
+        *
+        * This function exists for historical reasons, Database::update() has a more standard
+        * calling convention and feature set
+        */
+       function set( $table, $var, $value, $cond, $fname = 'Database::set' )
+       {
+               $table = $this->tableName( $table );
+               $sql = "UPDATE $table SET $var = '" .
+                 $this->strencode( $value ) . "' WHERE ($cond)";
+               return (bool)$this->query( $sql, $fname );
+       }
+
+       /**
+        * Simple SELECT wrapper, returns a single field, input must be encoded
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns FALSE on failure
+        */
+       function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+               if ( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               $options['LIMIT'] = 1;
+
+               $res = $this->select( $table, $var, $cond, $fname, $options );
+               if ( $res === false || !$this->numRows( $res ) ) {
+                       return false;
+               }
+               $row = $this->fetchRow( $res );
+               if ( $row !== false ) {
+                       $this->freeResult( $res );
+                       return $row[0];
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Returns an optional USE INDEX clause to go after the table, and a
+        * string to go at the end of the query
+        *
+        * @private
+        *
+        * @param array $options an associative array of options to be turned into
+        *              an SQL query, valid keys are listed in the function.
+        * @return array
+        */
+       function makeSelectOptions( $options ) {
+               $preLimitTail = $postLimitTail = '';
+               $startOpts = '';
+
+               $noKeyOptions = array();
+               foreach ( $options as $key => $option ) {
+                       if ( is_numeric( $key ) ) {
+                               $noKeyOptions[$option] = true;
+                       }
+               }
+
+               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+               
+               //if (isset($options['LIMIT'])) {
+               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
+               //              isset($options['OFFSET']) ? $options['OFFSET'] 
+               //              : false);
+               //}
+
+               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+               # Various MySQL extensions
+               if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+               if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+               if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+               if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+               if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+               if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+               if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+               if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+               if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+               } else {
+                       $useIndex = '';
+               }
+               
+               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+       }
+
+       /**
+        * SELECT wrapper
+        *
+        * @param mixed  $table   Array or string, table name(s) (prefix auto-added)
+        * @param mixed  $vars    Array or string, field name(s) to be retrieved
+        * @param mixed  $conds   Array or string, condition(s) for WHERE
+        * @param string $fname   Calling function name (use __METHOD__) for logs/profiling
+        * @param array  $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+        *                        see Database::makeSelectOptions code for list of supported stuff
+        * @param array $join_conds Associative array of table join conditions (optional)
+        *                        (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+        * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+        */
+       function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() )
+       {
+               $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
+               return $this->query( $sql, $fname );
+       }
+       
+       /**
+        * SELECT wrapper
+        *
+        * @param mixed  $table   Array or string, table name(s) (prefix auto-added)
+        * @param mixed  $vars    Array or string, field name(s) to be retrieved
+        * @param mixed  $conds   Array or string, condition(s) for WHERE
+        * @param string $fname   Calling function name (use __METHOD__) for logs/profiling
+        * @param array  $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+        *                        see Database::makeSelectOptions code for list of supported stuff
+        * @param array $join_conds Associative array of table join conditions (optional)
+        *                        (e.g. array( 'page' => array('LEFT JOIN','page_latest=rev_id') )
+        * @return string, the SQL text
+        */
+       function selectSQLText( $table, $vars, $conds='', $fname = 'Database::select', $options = array(), $join_conds = array() ) {
+               if( is_array( $vars ) ) {
+                       $vars = implode( ',', $vars );
+               }
+               if( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               if( is_array( $table ) ) {
+                       if ( !empty($join_conds) || ( isset( $options['USE INDEX'] ) && is_array( @$options['USE INDEX'] ) ) )
+                               $from = ' FROM ' . $this->tableNamesWithUseIndexOrJOIN( $table, @$options['USE INDEX'], $join_conds );
+                       else
+                               $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+               } elseif ($table!='') {
+                       if ($table{0}==' ') {
+                               $from = ' FROM ' . $table;
+                       } else {
+                               $from = ' FROM ' . $this->tableName( $table );
+                       }
+               } else {
+                       $from = '';
+               }
+
+               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+               if( !empty( $conds ) ) {
+                       if ( is_array( $conds ) ) {
+                               $conds = $this->makeList( $conds, LIST_AND );
+                       }
+                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+               } else {
+                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+               }
+
+               if (isset($options['LIMIT']))
+                       $sql = $this->limitResult($sql, $options['LIMIT'],
+                               isset($options['OFFSET']) ? $options['OFFSET'] : false);
+               $sql = "$sql $postLimitTail";
+               
+               if (isset($options['EXPLAIN'])) {
+                       $sql = 'EXPLAIN ' . $sql;
+               }
+               return $sql;
+       }
+
+       /**
+        * Single row SELECT wrapper
+        * Aborts or returns FALSE on error
+        *
+        * $vars: the selected variables
+        * $conds: a condition map, terms are ANDed together.
+        *   Items with numeric keys are taken to be literal conditions
+        * Takes an array of selected variables, and a condition map, which is ANDed
+        * e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
+        * NS_MAIN, "page_title" => "Astronomy" ) )   would return an object where
+        * $obj- >page_id is the ID of the Astronomy article
+        *
+        * @todo migrate documentation to phpdocumentor format
+        */
+       function selectRow( $table, $vars, $conds, $fname = 'Database::selectRow', $options = array(), $join_conds = array() ) {
+               $options['LIMIT'] = 1;
+               $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
+               if ( $res === false )
+                       return false;
+               if ( !$this->numRows($res) ) {
+                       $this->freeResult($res);
+                       return false;
+               }
+               $obj = $this->fetchObject( $res );
+               $this->freeResult( $res );
+               return $obj;
+
+       }
+       
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * Takes same arguments as Database::select()
+        */
+       
+       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+               $options['EXPLAIN']=true;
+               $res = $this->select ($table, $vars, $conds, $fname, $options );
+               if ( $res === false )
+                       return false;
+               if (!$this->numRows($res)) {
+                       $this->freeResult($res);
+                       return 0;
+               }
+               
+               $rows=1;
+       
+               while( $plan = $this->fetchObject( $res ) ) {
+                       $rows *= ($plan->rows > 0)?$plan->rows:1; // avoid resetting to zero
+               }
+               
+               $this->freeResult($res);
+               return $rows;           
+       }
+       
+
+       /**
+        * Removes most variables from an SQL query and replaces them with X or N for numbers.
+        * It's only slightly flawed. Don't use for anything important.
+        *
+        * @param string $sql A SQL Query
+        * @static
+        */
+       static function generalizeSQL( $sql ) {
+               # This does the same as the regexp below would do, but in such a way
+               # as to avoid crashing php on some large strings.
+               # $sql = preg_replace ( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql);
+
+               $sql = str_replace ( "\\\\", '', $sql);
+               $sql = str_replace ( "\\'", '', $sql);
+               $sql = str_replace ( "\\\"", '', $sql);
+               $sql = preg_replace ("/'.*'/s", "'X'", $sql);
+               $sql = preg_replace ('/".*"/s', "'X'", $sql);
+
+               # All newlines, tabs, etc replaced by single space
+               $sql = preg_replace ( '/\s+/', ' ', $sql);
+
+               # All numbers => N
+               $sql = preg_replace ('/-?[0-9]+/s', 'N', $sql);
+
+               return $sql;
+       }
+
+       /**
+        * Determines whether a field exists in a table
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns NULL on failure
+        */
+       function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+               $table = $this->tableName( $table );
+               $res = $this->query( 'DESCRIBE '.$table, $fname );
+               if ( !$res ) {
+                       return NULL;
+               }
+
+               $found = false;
+
+               while ( $row = $this->fetchObject( $res ) ) {
+                       if ( $row->Field == $field ) {
+                               $found = true;
+                               break;
+                       }
+               }
+               return $found;
+       }
+
+       /**
+        * Determines whether an index exists
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns NULL on failure
+        */
+       function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
+               $info = $this->indexInfo( $table, $index, $fname );
+               if ( is_null( $info ) ) {
+                       return NULL;
+               } else {
+                       return $info !== false;
+               }
+       }
+
+
+       /**
+        * Get information about an index into an object
+        * Returns false if the index does not exist
+        */
+       function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+               # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
+               # SHOW INDEX should work for 3.x and up:
+               # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
+               $table = $this->tableName( $table );
+               $sql = 'SHOW INDEX FROM '.$table;
+               $res = $this->query( $sql, $fname );
+               if ( !$res ) {
+                       return NULL;
+               }
+
+               $result = array();
+               while ( $row = $this->fetchObject( $res ) ) {
+                       if ( $row->Key_name == $index ) {
+                               $result[] = $row;
+                       }
+               }
+               $this->freeResult($res);
+               
+               return empty($result) ? false : $result;
+       }
+
+       /**
+        * Query whether a given table exists
+        */
+       function tableExists( $table ) {
+               $table = $this->tableName( $table );
+               $old = $this->ignoreErrors( true );
+               $res = $this->query( "SELECT 1 FROM $table LIMIT 1" );
+               $this->ignoreErrors( $old );
+               if( $res ) {
+                       $this->freeResult( $res );
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * mysql_fetch_field() wrapper
+        * Returns false if the field doesn't exist
+        *
+        * @param $table
+        * @param $field
+        */
+       function fieldInfo( $table, $field ) {
+               $table = $this->tableName( $table );
+               $res = $this->query( "SELECT * FROM $table LIMIT 1" );
+               $n = mysql_num_fields( $res->result );
+               for( $i = 0; $i < $n; $i++ ) {
+                       $meta = mysql_fetch_field( $res->result, $i );
+                       if( $field == $meta->name ) {
+                               return new MySQLField($meta);
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * mysql_field_type() wrapper
+        */
+       function fieldType( $res, $index ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mysql_field_type( $res, $index );
+       }
+
+       /**
+        * Determines if a given index is unique
+        */
+       function indexUnique( $table, $index ) {
+               $indexInfo = $this->indexInfo( $table, $index );
+               if ( !$indexInfo ) {
+                       return NULL;
+               }
+               return !$indexInfo[0]->Non_unique;
+       }
+
+       /**
+        * INSERT wrapper, inserts an array into a table
+        *
+        * $a may be a single associative array, or an array of these with numeric keys, for
+        * multi-row insert.
+        *
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns success
+        */
+       function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+               # No rows to insert, easy just return now
+               if ( !count( $a ) ) {
+                       return true;
+               }
+
+               $table = $this->tableName( $table );
+               if ( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+                       $multi = true;
+                       $keys = array_keys( $a[0] );
+               } else {
+                       $multi = false;
+                       $keys = array_keys( $a );
+               }
+
+               $sql = 'INSERT ' . implode( ' ', $options ) .
+                       " INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+               if ( $multi ) {
+                       $first = true;
+                       foreach ( $a as $row ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $sql .= ',';
+                               }
+                               $sql .= '(' . $this->makeList( $row ) . ')';
+                       }
+               } else {
+                       $sql .= '(' . $this->makeList( $a ) . ')';
+               }
+               return (bool)$this->query( $sql, $fname );
+       }
+
+       /**
+        * Make UPDATE options for the Database::update function
+        *
+        * @private
+        * @param array $options The options passed to Database::update
+        * @return string
+        */
+       function makeUpdateOptions( $options ) {
+               if( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               $opts = array();
+               if ( in_array( 'LOW_PRIORITY', $options ) )
+                       $opts[] = $this->lowPriorityOption();
+               if ( in_array( 'IGNORE', $options ) )
+                       $opts[] = 'IGNORE';
+               return implode(' ', $opts);
+       }
+
+       /**
+        * UPDATE wrapper, takes a condition array and a SET array
+        *
+        * @param string $table  The table to UPDATE
+        * @param array  $values An array of values to SET
+        * @param array  $conds  An array of conditions (WHERE). Use '*' to update all rows.
+        * @param string $fname  The Class::Function calling this function
+        *                       (for the log)
+        * @param array  $options An array of UPDATE options, can be one or
+        *                        more of IGNORE, LOW_PRIORITY
+        * @return bool
+        */
+       function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+               $table = $this->tableName( $table );
+               $opts = $this->makeUpdateOptions( $options );
+               $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
+               if ( $conds != '*' ) {
+                       $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+               }
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Makes an encoded list of strings from an array
+        * $mode:
+        *        LIST_COMMA         - comma separated, no field names
+        *        LIST_AND           - ANDed WHERE clause (without the WHERE)
+        *        LIST_OR            - ORed WHERE clause (without the WHERE)
+        *        LIST_SET           - comma separated with field names, like a SET clause
+        *        LIST_NAMES         - comma separated field names
+        */
+       function makeList( $a, $mode = LIST_COMMA ) {
+               if ( !is_array( $a ) ) {
+                       throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
+               }
+
+               $first = true;
+               $list = '';
+               foreach ( $a as $field => $value ) {
+                       if ( !$first ) {
+                               if ( $mode == LIST_AND ) {
+                                       $list .= ' AND ';
+                               } elseif($mode == LIST_OR) {
+                                       $list .= ' OR ';
+                               } else {
+                                       $list .= ',';
+                               }
+                       } else {
+                               $first = false;
+                       }
+                       if ( ($mode == LIST_AND || $mode == LIST_OR) && is_numeric( $field ) ) {
+                               $list .= "($value)";
+                       } elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
+                               $list .= "$value";
+                       } elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
+                               if( count( $value ) == 0 ) {
+                                       throw new MWException( __METHOD__.': empty input' );
+                               } elseif( count( $value ) == 1 ) {
+                                       // Special-case single values, as IN isn't terribly efficient
+                                       // Don't necessarily assume the single key is 0; we don't
+                                       // enforce linear numeric ordering on other arrays here.
+                                       $value = array_values( $value );
+                                       $list .= $field." = ".$this->addQuotes( $value[0] );
+                               } else {
+                                       $list .= $field." IN (".$this->makeList($value).") ";
+                               }
+                       } elseif( is_null($value) ) {
+                               if ( $mode == LIST_AND || $mode == LIST_OR ) {
+                                       $list .= "$field IS ";
+                               } elseif ( $mode == LIST_SET ) {
+                                       $list .= "$field = ";
+                               }
+                               $list .= 'NULL';
+                       } else {
+                               if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
+                                       $list .= "$field = ";
+                               }
+                               $list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
+                       }
+               }
+               return $list;
+       }
+
+       /**
+        * Change the current database
+        */
+       function selectDB( $db ) {
+               $this->mDBname = $db;
+               return mysql_select_db( $db, $this->mConn );
+       }
+
+       /**
+        * Get the current DB name
+        */
+       function getDBname() {
+               return $this->mDBname;
+       }
+
+       /**
+        * Get the server hostname or IP address
+        */
+       function getServer() {
+               return $this->mServer;
+       }
+
+       /**
+        * Format a table name ready for use in constructing an SQL query
+        *
+        * This does two important things: it quotes the table names to clean them up,
+        * and it adds a table prefix if only given a table name with no quotes.
+        *
+        * All functions of this object which require a table name call this function
+        * themselves. Pass the canonical name to such functions. This is only needed
+        * when calling query() directly.
+        *
+        * @param string $name database table name
+        * @return string full database name
+        */
+       function tableName( $name ) {
+               global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
+               # Skip the entire process when we have a string quoted on both ends.
+               # Note that we check the end so that we will still quote any use of
+               # use of `database`.table. But won't break things if someone wants
+               # to query a database table with a dot in the name.
+               if ( $name[0] == '`' && substr( $name, -1, 1 ) == '`' ) return $name;
+               
+               # Lets test for any bits of text that should never show up in a table
+               # name. Basically anything like JOIN or ON which are actually part of
+               # SQL queries, but may end up inside of the table value to combine
+               # sql. Such as how the API is doing.
+               # Note that we use a whitespace test rather than a \b test to avoid
+               # any remote case where a word like on may be inside of a table name
+               # surrounded by symbols which may be considered word breaks.
+               if( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) return $name;
+               
+               # Split database and table into proper variables.
+               # We reverse the explode so that database.table and table both output
+               # the correct table.
+               $dbDetails = array_reverse( explode( '.', $name, 2 ) );
+               if( isset( $dbDetails[1] ) ) @list( $table, $database ) = $dbDetails;
+               else                         @list( $table ) = $dbDetails;
+               $prefix = $this->mTablePrefix; # Default prefix
+               
+               # A database name has been specified in input. Quote the table name
+               # because we don't want any prefixes added.
+               if( isset($database) ) $table = ( $table[0] == '`' ? $table : "`{$table}`" );
+               
+               # Note that we use the long format because php will complain in in_array if
+               # the input is not an array, and will complain in is_array if it is not set.
+               if( !isset( $database ) # Don't use shared database if pre selected.
+                && isset( $wgSharedDB ) # We have a shared database
+                && $table[0] != '`' # Paranoia check to prevent shared tables listing '`table`'
+                && isset( $wgSharedTables )
+                && is_array( $wgSharedTables )
+                && in_array( $table, $wgSharedTables ) ) { # A shared table is selected
+                       $database = $wgSharedDB;
+                       $prefix   = isset( $wgSharedprefix ) ? $wgSharedprefix : $prefix;
+               }
+               
+               # Quote the $database and $table and apply the prefix if not quoted.
+               if( isset($database) ) $database = ( $database[0] == '`' ? $database : "`{$database}`" );
+               $table = ( $table[0] == '`' ? $table : "`{$prefix}{$table}`" );
+               
+               # Merge our database and table into our final table name.
+               $tableName = ( isset($database) ? "{$database}.{$table}" : "{$table}" );
+               
+               # We're finished, return.
+               return $tableName;
+       }
+
+       /**
+        * Fetch a number of table names into an array
+        * This is handy when you need to construct SQL for joins
+        *
+        * Example:
+        * extract($dbr->tableNames('user','watchlist'));
+        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+        */
+       public function tableNames() {
+               $inArray = func_get_args();
+               $retVal = array();
+               foreach ( $inArray as $name ) {
+                       $retVal[$name] = $this->tableName( $name );
+               }
+               return $retVal;
+       }
+       
+       /**
+        * Fetch a number of table names into an zero-indexed numerical array
+        * This is handy when you need to construct SQL for joins
+        *
+        * Example:
+        * list( $user, $watchlist ) = $dbr->tableNamesN('user','watchlist');
+        * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
+        *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
+        */
+       public function tableNamesN() {
+               $inArray = func_get_args();
+               $retVal = array();
+               foreach ( $inArray as $name ) {
+                       $retVal[] = $this->tableName( $name );
+               }
+               return $retVal;
+       }
+
+       /**
+        * @private
+        */
+       function tableNamesWithUseIndexOrJOIN( $tables, $use_index = array(), $join_conds = array() ) {
+               $ret = array();
+               $retJOIN = array();
+               $use_index_safe = is_array($use_index) ? $use_index : array();
+               $join_conds_safe = is_array($join_conds) ? $join_conds : array();
+               foreach ( $tables as $table ) {
+                       // Is there a JOIN and INDEX clause for this table?
+                       if ( isset($join_conds_safe[$table]) && isset($use_index_safe[$table]) ) {
+                               $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+                               $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+                               $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+                               $retJOIN[] = $tableClause;
+                       // Is there an INDEX clause?
+                       } else if ( isset($use_index_safe[$table]) ) {
+                               $tableClause = $this->tableName( $table );
+                               $tableClause .= ' ' . $this->useIndexClause( implode( ',', (array)$use_index_safe[$table] ) );
+                               $ret[] = $tableClause;
+                       // Is there a JOIN clause?
+                       } else if ( isset($join_conds_safe[$table]) ) {
+                               $tableClause = $join_conds_safe[$table][0] . ' ' . $this->tableName( $table );
+                               $tableClause .= ' ON (' . $this->makeList((array)$join_conds_safe[$table][1], LIST_AND) . ')';
+                               $retJOIN[] = $tableClause;
+                       } else {
+                               $tableClause = $this->tableName( $table );
+                               $ret[] = $tableClause;
+                       }
+               }
+               // We can't separate explicit JOIN clauses with ',', use ' ' for those
+               $straightJoins = !empty($ret) ? implode( ',', $ret ) : "";
+               $otherJoins = !empty($retJOIN) ? implode( ' ', $retJOIN ) : "";
+               // Compile our final table clause
+               return implode(' ',array($straightJoins,$otherJoins) );
+       }
+
+       /**
+        * Wrapper for addslashes()
+        * @param string $s String to be slashed.
+        * @return string slashed string.
+        */
+       function strencode( $s ) {
+               return mysql_real_escape_string( $s, $this->mConn );
+       }
+
+       /**
+        * If it's a string, adds quotes and backslashes
+        * Otherwise returns as-is
+        */
+       function addQuotes( $s ) {
+               if ( is_null( $s ) ) {
+                       return 'NULL';
+               } else {
+                       # This will also quote numeric values. This should be harmless,
+                       # and protects against weird problems that occur when they really
+                       # _are_ strings such as article titles and string->number->string
+                       # conversion is not 1:1.
+                       return "'" . $this->strencode( $s ) . "'";
+               }
+       }
+
+       /**
+        * Escape string for safe LIKE usage
+        */
+       function escapeLike( $s ) {
+               $s=$this->strencode( $s );
+               $s=str_replace(array('%','_'),array('\%','\_'),$s);
+               return $s;
+       }
+
+       /**
+        * Returns an appropriately quoted sequence value for inserting a new row.
+        * MySQL has autoincrement fields, so this is just NULL. But the PostgreSQL
+        * subclass will return an integer, and save the value for insertId()
+        */
+       function nextSequenceValue( $seqName ) {
+               return NULL;
+       }
+
+       /**
+        * USE INDEX clause
+        * PostgreSQL doesn't have them and returns ""
+        */
+       function useIndexClause( $index ) {
+               return "FORCE INDEX ($index)";
+       }
+
+       /**
+        * REPLACE query wrapper
+        * PostgreSQL simulates this with a DELETE followed by INSERT
+        * $row is the row to insert, an associative array
+        * $uniqueIndexes is an array of indexes. Each element may be either a
+        * field name or an array of field names
+        *
+        * It may be more efficient to leave off unique indexes which are unlikely to collide.
+        * However if you do this, you run the risk of encountering errors which wouldn't have
+        * occurred in MySQL
+        *
+        * @todo migrate comment to phodocumentor format
+        */
+       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+               $table = $this->tableName( $table );
+
+               # Single row case
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = array( $rows );
+               }
+
+               $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+               $first = true;
+               foreach ( $rows as $row ) {
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $sql .= ',';
+                       }
+                       $sql .= '(' . $this->makeList( $row ) . ')';
+               }
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * DELETE where the condition is a join
+        * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
+        *
+        * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+        * join condition matches, set $conds='*'
+        *
+        * DO NOT put the join condition in $conds
+        *
+        * @param string $delTable The table to delete from.
+        * @param string $joinTable The other table.
+        * @param string $delVar The variable to join on, in the first table.
+        * @param string $joinVar The variable to join on, in the second table.
+        * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+        */
+       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
+               }
+
+               $delTable = $this->tableName( $delTable );
+               $joinTable = $this->tableName( $joinTable );
+               $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+               if ( $conds != '*' ) {
+                       $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+               }
+
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Returns the size of a text field, or -1 for "unlimited"
+        */
+       function textFieldSize( $table, $field ) {
+               $table = $this->tableName( $table );
+               $sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
+               $res = $this->query( $sql, 'Database::textFieldSize' );
+               $row = $this->fetchObject( $res );
+               $this->freeResult( $res );
+
+               $m = array();
+               if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+                       $size = $m[1];
+               } else {
+                       $size = -1;
+               }
+               return $size;
+       }
+
+       /**
+        * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+        */
+       function lowPriorityOption() {
+               return 'LOW_PRIORITY';
+       }
+
+       /**
+        * DELETE query wrapper
+        *
+        * Use $conds == "*" to delete all rows
+        */
+       function delete( $table, $conds, $fname = 'Database::delete' ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError( $this, 'Database::delete() called with no conditions' );
+               }
+               $table = $this->tableName( $table );
+               $sql = "DELETE FROM $table";
+               if ( $conds != '*' ) {
+                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+               }
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * INSERT SELECT wrapper
+        * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+        * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+        * $conds may be "*" to copy the whole table
+        * srcTable may be an array of tables.
+        */
+       function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+               $insertOptions = array(), $selectOptions = array() )
+       {
+               $destTable = $this->tableName( $destTable );
+               if ( is_array( $insertOptions ) ) {
+                       $insertOptions = implode( ' ', $insertOptions );
+               }
+               if( !is_array( $selectOptions ) ) {
+                       $selectOptions = array( $selectOptions );
+               }
+               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+               if( is_array( $srcTable ) ) {
+                       $srcTable =  implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+               } else {
+                       $srcTable = $this->tableName( $srcTable );
+               }
+               $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+                       " SELECT $startOpts " . implode( ',', $varMap ) .
+                       " FROM $srcTable $useIndex ";
+               if ( $conds != '*' ) {
+                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+               }
+               $sql .= " $tailOpts";
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Construct a LIMIT query with optional offset
+        * This is used for query pages
+        * $sql string SQL query we will append the limit too
+        * $limit integer the SQL limit
+        * $offset integer the SQL offset (default false)
+        */
+       function limitResult($sql, $limit, $offset=false) {
+               if( !is_numeric($limit) ) {
+                       throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+               }
+               return "$sql LIMIT "
+                               . ( (is_numeric($offset) && $offset != 0) ? "{$offset}," : "" )
+                               . "{$limit} ";
+       }
+       function limitResultForUpdate($sql, $num) {
+               return $this->limitResult($sql, $num, 0);
+       }
+
+       /**
+        * Returns an SQL expression for a simple conditional.
+        * Uses IF on MySQL.
+        *
+        * @param string $cond SQL expression which will result in a boolean value
+        * @param string $trueVal SQL expression to return if true
+        * @param string $falseVal SQL expression to return if false
+        * @return string SQL fragment
+        */
+       function conditional( $cond, $trueVal, $falseVal ) {
+               return " IF($cond, $trueVal, $falseVal) ";
+       }
+
+       /**
+        * Returns a comand for str_replace function in SQL query.
+        * Uses REPLACE() in MySQL
+        *
+        * @param string $orig String or column to modify
+        * @param string $old String or column to seek
+        * @param string $new String or column to replace with
+        */
+       function strreplace( $orig, $old, $new ) {
+               return "REPLACE({$orig}, {$old}, {$new})";
+       }
+
+       /**
+        * Determines if the last failure was due to a deadlock
+        */
+       function wasDeadlock() {
+               return $this->lastErrno() == 1213;
+       }
+
+       /**
+        * Perform a deadlock-prone transaction.
+        *
+        * This function invokes a callback function to perform a set of write
+        * queries. If a deadlock occurs during the processing, the transaction
+        * will be rolled back and the callback function will be called again.
+        *
+        * Usage:
+        *   $dbw->deadlockLoop( callback, ... );
+        *
+        * Extra arguments are passed through to the specified callback function.
+        *
+        * Returns whatever the callback function returned on its successful,
+        * iteration, or false on error, for example if the retry limit was
+        * reached.
+        */
+       function deadlockLoop() {
+               $myFname = 'Database::deadlockLoop';
+
+               $this->begin();
+               $args = func_get_args();
+               $function = array_shift( $args );
+               $oldIgnore = $this->ignoreErrors( true );
+               $tries = DEADLOCK_TRIES;
+               if ( is_array( $function ) ) {
+                       $fname = $function[0];
+               } else {
+                       $fname = $function;
+               }
+               do {
+                       $retVal = call_user_func_array( $function, $args );
+                       $error = $this->lastError();
+                       $errno = $this->lastErrno();
+                       $sql = $this->lastQuery();
+
+                       if ( $errno ) {
+                               if ( $this->wasDeadlock() ) {
+                                       # Retry
+                                       usleep( mt_rand( DEADLOCK_DELAY_MIN, DEADLOCK_DELAY_MAX ) );
+                               } else {
+                                       $this->reportQueryError( $error, $errno, $sql, $fname );
+                               }
+                       }
+               } while( $this->wasDeadlock() && --$tries > 0 );
+               $this->ignoreErrors( $oldIgnore );
+               if ( $tries <= 0 ) {
+                       $this->query( 'ROLLBACK', $myFname );
+                       $this->reportQueryError( $error, $errno, $sql, $fname );
+                       return false;
+               } else {
+                       $this->query( 'COMMIT', $myFname );
+                       return $retVal;
+               }
+       }
+
+       /**
+        * Do a SELECT MASTER_POS_WAIT()
+        *
+        * @param string $file the binlog file
+        * @param string $pos the binlog position
+        * @param integer $timeout the maximum number of seconds to wait for synchronisation
+        */
+       function masterPosWait( MySQLMasterPos $pos, $timeout ) {
+               $fname = 'Database::masterPosWait';
+               wfProfileIn( $fname );
+
+               # Commit any open transactions
+               if ( $this->mTrxLevel ) {
+                       $this->immediateCommit();
+               }
+
+               if ( !is_null( $this->mFakeSlaveLag ) ) {
+                       $wait = intval( ( $pos->pos - microtime(true) + $this->mFakeSlaveLag ) * 1e6 );
+                       if ( $wait > $timeout * 1e6 ) {
+                               wfDebug( "Fake slave timed out waiting for $pos ($wait us)\n" );
+                               wfProfileOut( $fname );
+                               return -1;
+                       } elseif ( $wait > 0 ) {
+                               wfDebug( "Fake slave waiting $wait us\n" );
+                               usleep( $wait );
+                               wfProfileOut( $fname );
+                               return 1;
+                       } else {
+                               wfDebug( "Fake slave up to date ($wait us)\n" );
+                               wfProfileOut( $fname );
+                               return 0;
+                       }
+               }
+
+               # Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
+               $encFile = $this->addQuotes( $pos->file );
+               $encPos = intval( $pos->pos );
+               $sql = "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)";
+               $res = $this->doQuery( $sql );
+               if ( $res && $row = $this->fetchRow( $res ) ) {
+                       $this->freeResult( $res );
+                       wfProfileOut( $fname );
+                       return $row[0];
+               } else {
+                       wfProfileOut( $fname );
+                       return false;
+               }
+       }
+
+       /**
+        * Get the position of the master from SHOW SLAVE STATUS
+        */
+       function getSlavePos() {
+               if ( !is_null( $this->mFakeSlaveLag ) ) {
+                       $pos = new MySQLMasterPos( 'fake', microtime(true) - $this->mFakeSlaveLag );
+                       wfDebug( __METHOD__.": fake slave pos = $pos\n" );
+                       return $pos;
+               }
+               $res = $this->query( 'SHOW SLAVE STATUS', 'Database::getSlavePos' );
+               $row = $this->fetchObject( $res );
+               if ( $row ) {
+                       return new MySQLMasterPos( $row->Master_Log_File, $row->Read_Master_Log_Pos );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Get the position of the master from SHOW MASTER STATUS
+        */
+       function getMasterPos() {
+               if ( $this->mFakeMaster ) {
+                       return new MySQLMasterPos( 'fake', microtime( true ) );
+               }
+               $res = $this->query( 'SHOW MASTER STATUS', 'Database::getMasterPos' );
+               $row = $this->fetchObject( $res );
+               if ( $row ) {
+                       return new MySQLMasterPos( $row->File, $row->Position );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Begin a transaction, committing any previously open transaction
+        */
+       function begin( $fname = 'Database::begin' ) {
+               $this->query( 'BEGIN', $fname );
+               $this->mTrxLevel = 1;
+       }
+
+       /**
+        * End a transaction
+        */
+       function commit( $fname = 'Database::commit' ) {
+               $this->query( 'COMMIT', $fname );
+               $this->mTrxLevel = 0;
+       }
+
+       /**
+        * Rollback a transaction.
+        * No-op on non-transactional databases.
+        */
+       function rollback( $fname = 'Database::rollback' ) {
+               $this->query( 'ROLLBACK', $fname, true );
+               $this->mTrxLevel = 0;
+       }
+
+       /**
+        * Begin a transaction, committing any previously open transaction
+        * @deprecated use begin()
+        */
+       function immediateBegin( $fname = 'Database::immediateBegin' ) {
+               $this->begin();
+       }
+
+       /**
+        * Commit transaction, if one is open
+        * @deprecated use commit()
+        */
+       function immediateCommit( $fname = 'Database::immediateCommit' ) {
+               $this->commit();
+       }
+
+       /**
+        * Return MW-style timestamp used for MySQL schema
+        */
+       function timestamp( $ts=0 ) {
+               return wfTimestamp(TS_MW,$ts);
+       }
+
+       /**
+        * Local database timestamp format or null
+        */
+       function timestampOrNull( $ts = null ) {
+               if( is_null( $ts ) ) {
+                       return null;
+               } else {
+                       return $this->timestamp( $ts );
+               }
+       }
+
+       /**
+        * @todo document
+        */
+       function resultObject( $result ) {
+               if( empty( $result ) ) {
+                       return false;
+               } elseif ( $result instanceof ResultWrapper ) {
+                       return $result;
+               } elseif ( $result === true ) {
+                       // Successful write query
+                       return $result;
+               } else {
+                       return new ResultWrapper( $this, $result );
+               }
+       }
+
+       /**
+        * Return aggregated value alias
+        */
+       function aggregateValue ($valuedata,$valuename='value') {
+               return $valuename;
+       }
+
+       /**
+        * @return string wikitext of a link to the server software's web site
+        */
+       function getSoftwareLink() {
+               return "[http://www.mysql.com/ MySQL]";
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               return mysql_get_server_info( $this->mConn );
+       }
+
+       /**
+        * Ping the server and try to reconnect if it there is no connection
+        */
+       function ping() {
+               if( !function_exists( 'mysql_ping' ) ) {
+                       wfDebug( "Tried to call mysql_ping but this is ancient PHP version. Faking it!\n" );
+                       return true;
+               }
+               $ping = mysql_ping( $this->mConn );
+               if ( $ping ) {
+                       return true;
+               }
+
+               // Need to reconnect manually in MySQL client 5.0.13+
+               if ( version_compare( mysql_get_client_info(), '5.0.13', '>=' ) ) {
+                       mysql_close( $this->mConn );
+                       $this->mOpened = false;
+                       $this->mConn = false;
+                       $this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * Get slave lag.
+        * At the moment, this will only work if the DB user has the PROCESS privilege
+        */
+       function getLag() {
+               if ( !is_null( $this->mFakeSlaveLag ) ) {
+                       wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
+                       return $this->mFakeSlaveLag;
+               }
+               $res = $this->query( 'SHOW PROCESSLIST' );
+               # Find slave SQL thread
+               while ( $row = $this->fetchObject( $res ) ) {
+                       /* This should work for most situations - when default db 
+                        * for thread is not specified, it had no events executed, 
+                        * and therefore it doesn't know yet how lagged it is.
+                        *
+                        * Relay log I/O thread does not select databases.
+                        */
+                       if ( $row->User == 'system user' && 
+                               $row->State != 'Waiting for master to send event' &&
+                               $row->State != 'Connecting to master' && 
+                               $row->State != 'Queueing master event to the relay log' &&
+                               $row->State != 'Waiting for master update' &&
+                               $row->State != 'Requesting binlog dump'
+                               ) {
+                               # This is it, return the time (except -ve)
+                               if ( $row->Time > 0x7fffffff ) {
+                                       return false;
+                               } else {
+                                       return $row->Time;
+                               }
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Get status information from SHOW STATUS in an associative array
+        */
+       function getStatus($which="%") {
+               $res = $this->query( "SHOW STATUS LIKE '{$which}'" );
+               $status = array();
+               while ( $row = $this->fetchObject( $res ) ) {
+                       $status[$row->Variable_name] = $row->Value;
+               }
+               return $status;
+       }
+
+       /**
+        * Return the maximum number of items allowed in a list, or 0 for unlimited.
+        */
+       function maxListLen() {
+               return 0;
+       }
+
+       function encodeBlob($b) {
+               return $b;
+       }
+
+       function decodeBlob($b) {
+               return $b;
+       }
+
+       /**
+        * Override database's default connection timeout.
+        * May be useful for very long batch queries such as
+        * full-wiki dumps, where a single query reads out
+        * over hours or days.
+        * @param int $timeout in seconds
+        */
+       public function setTimeout( $timeout ) {
+               $this->query( "SET net_read_timeout=$timeout" );
+               $this->query( "SET net_write_timeout=$timeout" );
+       }
+
+       /**
+        * Read and execute SQL commands from a file.
+        * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
+        * @param string $filename File name to open
+        * @param callback $lineCallback Optional function called before reading each line
+        * @param callback $resultCallback Optional function called for each MySQL result
+        */
+       function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
+               $fp = fopen( $filename, 'r' );
+               if ( false === $fp ) {
+                       throw new MWException( "Could not open \"{$filename}\".\n" );
+               }
+               $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+               fclose( $fp );
+               return $error;
+       }
+
+       /**
+        * Read and execute commands from an open file handle
+        * Returns true on success, error string or exception on failure (depending on object's error ignore settings)
+        * @param string $fp File handle
+        * @param callback $lineCallback Optional function called before reading each line
+        * @param callback $resultCallback Optional function called for each MySQL result
+        */
+       function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
+               $cmd = "";
+               $done = false;
+               $dollarquote = false;
+
+               while ( ! feof( $fp ) ) {
+                       if ( $lineCallback ) {
+                               call_user_func( $lineCallback );
+                       }
+                       $line = trim( fgets( $fp, 1024 ) );
+                       $sl = strlen( $line ) - 1;
+
+                       if ( $sl < 0 ) { continue; }
+                       if ( '-' == $line{0} && '-' == $line{1} ) { continue; }
+
+                       ## Allow dollar quoting for function declarations
+                       if (substr($line,0,4) == '$mw$') {
+                               if ($dollarquote) {
+                                       $dollarquote = false;
+                                       $done = true;
+                               }
+                               else {
+                                       $dollarquote = true;
+                               }
+                       }
+                       else if (!$dollarquote) {
+                               if ( ';' == $line{$sl} && ($sl < 2 || ';' != $line{$sl - 1})) {
+                                       $done = true;
+                                       $line = substr( $line, 0, $sl );
+                               }
+                       }
+
+                       if ( '' != $cmd ) { $cmd .= ' '; }
+                       $cmd .= "$line\n";
+
+                       if ( $done ) {
+                               $cmd = str_replace(';;', ";", $cmd);
+                               $cmd = $this->replaceVars( $cmd );
+                               $res = $this->query( $cmd, __METHOD__ );
+                               if ( $resultCallback ) {
+                                       call_user_func( $resultCallback, $res );
+                               }
+
+                               if ( false === $res ) {
+                                       $err = $this->lastError();
+                                       return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+                               }
+
+                               $cmd = '';
+                               $done = false;
+                       }
+               }
+               return true;
+       }
+
+
+       /**
+        * Replace variables in sourced SQL
+        */
+       protected function replaceVars( $ins ) {
+               $varnames = array(
+                       'wgDBserver', 'wgDBname', 'wgDBintlname', 'wgDBuser',
+                       'wgDBpassword', 'wgDBsqluser', 'wgDBsqlpassword',
+                       'wgDBadminuser', 'wgDBadminpassword', 'wgDBTableOptions',
+               );
+
+               // Ordinary variables
+               foreach ( $varnames as $var ) {
+                       if( isset( $GLOBALS[$var] ) ) {
+                               $val = addslashes( $GLOBALS[$var] ); // FIXME: safety check?
+                               $ins = str_replace( '{$' . $var . '}', $val, $ins );
+                               $ins = str_replace( '/*$' . $var . '*/`', '`' . $val, $ins );
+                               $ins = str_replace( '/*$' . $var . '*/', $val, $ins );
+                       }
+               }
+
+               // Table prefixes
+               $ins = preg_replace_callback( '/\/\*(?:\$wgDBprefix|_)\*\/([a-zA-Z_0-9]*)/',
+                       array( &$this, 'tableNameCallback' ), $ins );
+               return $ins;
+       }
+
+       /**
+        * Table name callback
+        * @private
+        */
+       protected function tableNameCallback( $matches ) {
+               return $this->tableName( $matches[1] );
+       }
+
+       /*
+        * Build a concatenation list to feed into a SQL query
+       */
+       function buildConcat( $stringList ) {
+               return 'CONCAT(' . implode( ',', $stringList ) . ')';
+       }
+       
+       /**
+        * Acquire a lock
+        * 
+        * Abstracted from Filestore::lock() so child classes can implement for
+        * their own needs.
+        * 
+        * @param string $lockName Name of lock to aquire
+        * @param string $method Name of method calling us
+        * @return bool
+        */
+       public function lock( $lockName, $method ) {
+               $lockName = $this->addQuotes( $lockName );
+               $result = $this->query( "SELECT GET_LOCK($lockName, 5) AS lockstatus", $method );
+               $row = $this->fetchObject( $result );
+               $this->freeResult( $result );
+
+               if( $row->lockstatus == 1 ) {
+                       return true;
+               } else {
+                       wfDebug( __METHOD__." failed to acquire lock\n" );
+                       return false;
+               }
+       }
+       /**
+        * Release a lock.
+        * 
+        * @todo fixme - Figure out a way to return a bool
+        * based on successful lock release.
+        * 
+        * @param string $lockName Name of lock to release
+        * @param string $method Name of method calling us
+        */
+       public function unlock( $lockName, $method ) {
+               $lockName = $this->addQuotes( $lockName );
+               $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
+               $this->freeResult( $result );
+       }
+}
+
+/**
+ * Database abstraction object for mySQL
+ * Inherit all methods and properties of Database::Database()
+ *
+ * @ingroup Database
+ * @see Database
+ */
+class DatabaseMysql extends Database {
+       # Inherit all
+}
+
+/******************************************************************************
+ * Utility classes
+ *****************************************************************************/
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class DBObject {
+       public $mData;
+
+       function DBObject($data) {
+               $this->mData = $data;
+       }
+
+       function isLOB() {
+               return false;
+       }
+
+       function data() {
+               return $this->mData;
+       }
+}
+
+/**
+ * Utility class
+ * @ingroup Database
+ *
+ * This allows us to distinguish a blob from a normal string and an array of strings
+ */
+class Blob {
+       private $mData;
+       function __construct($data) {
+               $this->mData = $data;
+       }
+       function fetch() {
+               return $this->mData;
+       }
+}
+
+/**
+ * Utility class.
+ * @ingroup Database
+ */
+class MySQLField {
+       private $name, $tablename, $default, $max_length, $nullable,
+               $is_pk, $is_unique, $is_multiple, $is_key, $type;
+       function __construct ($info) {
+               $this->name = $info->name;
+               $this->tablename = $info->table;
+               $this->default = $info->def;
+               $this->max_length = $info->max_length;
+               $this->nullable = !$info->not_null;
+               $this->is_pk = $info->primary_key;
+               $this->is_unique = $info->unique_key;
+               $this->is_multiple = $info->multiple_key;
+               $this->is_key = ($this->is_pk || $this->is_unique || $this->is_multiple);
+               $this->type = $info->type;
+       }
+
+       function name() {
+               return $this->name;
+       }
+
+       function tableName() {
+               return $this->tableName;
+       }
+
+       function defaultValue() {
+               return $this->default;
+       }
+
+       function maxLength() {
+               return $this->max_length;
+       }
+
+       function nullable() {
+               return $this->nullable;
+       }
+
+       function isKey() {
+               return $this->is_key;
+       }
+
+       function isMultipleKey() {
+               return $this->is_multiple;
+       }
+
+       function type() {
+               return $this->type;
+       }
+}
+
+/******************************************************************************
+ * Error classes
+ *****************************************************************************/
+
+/**
+ * Database error base class
+ * @ingroup Database
+ */
+class DBError extends MWException {
+       public $db;
+
+       /**
+        * Construct a database error
+        * @param Database $db The database object which threw the error
+        * @param string $error A simple error message to be used for debugging
+        */
+       function __construct( Database &$db, $error ) {
+               $this->db =& $db;
+               parent::__construct( $error );
+       }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBConnectionError extends DBError {
+       public $error;
+       
+       function __construct( Database &$db, $error = 'unknown error' ) {
+               $msg = 'DB connection error';
+               if ( trim( $error ) != '' ) {
+                       $msg .= ": $error";
+               }
+               $this->error = $error;
+               parent::__construct( $db, $msg );
+       }
+
+       function useOutputPage() {
+               // Not likely to work
+               return false;
+       }
+
+       function useMessageCache() {
+               // Not likely to work
+               return false;
+       }
+       
+       function getText() {
+               return $this->getMessage() . "\n";
+       }
+
+       function getLogMessage() {
+               # Don't send to the exception log
+               return false;
+       }
+
+       function getPageTitle() {
+               global $wgSitename;
+               return "$wgSitename has a problem";
+       }
+
+       function getHTML() {
+               global $wgTitle, $wgUseFileCache, $title, $wgInputEncoding;
+               global $wgSitename, $wgServer, $wgMessageCache;
+
+               # I give up, Brion is right. Getting the message cache to work when there is no DB is tricky.
+               # Hard coding strings instead.
+
+               $noconnect = "<p><strong>Sorry! This site is experiencing technical difficulties.</strong></p><p>Try waiting a few minutes and reloading.</p><p><small>(Can't contact the database server: $1)</small></p>";
+               $mainpage = 'Main Page';
+               $searchdisabled = <<<EOT
+<p style="margin: 1.5em 2em 1em">$wgSitename search is disabled for performance reasons. You can search via Google in the meantime.
+<span style="font-size: 89%; display: block; margin-left: .2em">Note that their indexes of $wgSitename content may be out of date.</span></p>',
+EOT;
+
+               $googlesearch = "
+<!-- SiteSearch Google -->
+<FORM method=GET action=\"http://www.google.com/search\">
+<TABLE bgcolor=\"#FFFFFF\"><tr><td>
+<A HREF=\"http://www.google.com/\">
+<IMG SRC=\"http://www.google.com/logos/Logo_40wht.gif\"
+border=\"0\" ALT=\"Google\"></A>
+</td>
+<td>
+<INPUT TYPE=text name=q size=31 maxlength=255 value=\"$1\">
+<INPUT type=submit name=btnG VALUE=\"Google Search\">
+<font size=-1>
+<input type=hidden name=domains value=\"$wgServer\"><br /><input type=radio name=sitesearch value=\"\"> WWW <input type=radio name=sitesearch value=\"$wgServer\" checked> $wgServer <br />
+<input type='hidden' name='ie' value='$2'>
+<input type='hidden' name='oe' value='$2'>
+</font>
+</td></tr></TABLE>
+</FORM>
+<!-- SiteSearch Google -->";
+               $cachederror = "The following is a cached copy of the requested page, and may not be up to date. ";
+
+               # No database access
+               if ( is_object( $wgMessageCache ) ) {
+                       $wgMessageCache->disable();
+               }
+
+               if ( trim( $this->error ) == '' ) {
+                       $this->error = $this->db->getProperty('mServer');
+               }
+
+               $text = str_replace( '$1', $this->error, $noconnect );
+               $text .= wfGetSiteNotice();
+
+               if($wgUseFileCache) {
+                       if($wgTitle) {
+                               $t =& $wgTitle;
+                       } else {
+                               if($title) {
+                                       $t = Title::newFromURL( $title );
+                               } elseif (@/**/$_REQUEST['search']) {
+                                       $search = $_REQUEST['search'];
+                                       return $searchdisabled .
+                                         str_replace( array( '$1', '$2' ), array( htmlspecialchars( $search ),
+                                         $wgInputEncoding ), $googlesearch );
+                               } else {
+                                       $t = Title::newFromText( $mainpage );
+                               }
+                       }
+
+                       $cache = new HTMLFileCache( $t );
+                       if( $cache->isFileCached() ) {
+                               // @todo, FIXME: $msg is not defined on the next line.
+                               $msg = '<p style="color: red"><b>'.$msg."<br />\n" .
+                                       $cachederror . "</b></p>\n";
+
+                               $tag = '<div id="article">';
+                               $text = str_replace(
+                                       $tag,
+                                       $tag . $msg,
+                                       $cache->fetchPageText() );
+                       }
+               }
+
+               return $text;
+       }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBQueryError extends DBError {
+       public $error, $errno, $sql, $fname;
+       
+       function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+               $message = "A database error has occurred\n" .
+                 "Query: $sql\n" .
+                 "Function: $fname\n" .
+                 "Error: $errno $error\n";
+
+               parent::__construct( $db, $message );
+               $this->error = $error;
+               $this->errno = $errno;
+               $this->sql = $sql;
+               $this->fname = $fname;
+       }
+
+       function getText() {
+               if ( $this->useMessageCache() ) {
+                       return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+               } else {
+                       return $this->getMessage();
+               }
+       }
+       
+       function getSQL() {
+               global $wgShowSQLErrors;
+               if( !$wgShowSQLErrors ) {
+                       return $this->msg( 'sqlhidden', 'SQL hidden' );
+               } else {
+                       return $this->sql;
+               }
+       }
+       
+       function getLogMessage() {
+               # Don't send to the exception log
+               return false;
+       }
+
+       function getPageTitle() {
+               return $this->msg( 'databaseerror', 'Database error' );
+       }
+
+       function getHTML() {
+               if ( $this->useMessageCache() ) {
+                       return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+                         htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
+               } else {
+                       return nl2br( htmlspecialchars( $this->getMessage() ) );
+               }
+       }
+}
+
+/**
+ * @ingroup Database
+ */
+class DBUnexpectedError extends DBError {}
+
+
+/**
+ * Result wrapper for grabbing data queried by someone else
+ * @ingroup Database
+ */
+class ResultWrapper implements Iterator {
+       var $db, $result, $pos = 0, $currentRow = null;
+
+       /**
+        * Create a new result object from a result resource and a Database object
+        */
+       function ResultWrapper( $database, $result ) {
+               $this->db = $database;
+               if ( $result instanceof ResultWrapper ) {
+                       $this->result = $result->result;
+               } else {
+                       $this->result = $result;
+               }
+       }
+
+       /**
+        * Get the number of rows in a result object
+        */
+       function numRows() {
+               return $this->db->numRows( $this->result );
+       }
+
+       /**
+        * Fetch the next row from the given result object, in object form.
+        * Fields can be retrieved with $row->fieldname, with fields acting like
+        * member variables.
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
+        */
+       function fetchObject() {
+               return $this->db->fetchObject( $this->result );
+       }
+
+       /**
+        * Fetch the next row from the given result object, in associative array
+        * form.  Fields are retrieved with $row['fieldname'].
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
+        */
+       function fetchRow() {
+               return $this->db->fetchRow( $this->result );
+       }
+
+       /**
+        * Free a result object
+        */
+       function free() {
+               $this->db->freeResult( $this->result );
+               unset( $this->result );
+               unset( $this->db );
+       }
+
+       /**
+        * Change the position of the cursor in a result object
+        * See mysql_data_seek()
+        */
+       function seek( $row ) {
+               $this->db->dataSeek( $this->result, $row );
+       }
+
+       /*********************
+        * Iterator functions
+        * Note that using these in combination with the non-iterator functions
+        * above may cause rows to be skipped or repeated.
+        */
+
+       function rewind() {
+               if ($this->numRows()) {
+                       $this->db->dataSeek($this->result, 0);
+               }
+               $this->pos = 0;
+               $this->currentRow = null;
+       }
+
+       function current() {
+               if ( is_null( $this->currentRow ) ) {
+                       $this->next();
+               }
+               return $this->currentRow;
+       }
+
+       function key() {
+               return $this->pos;
+       }
+
+       function next() {
+               $this->pos++;
+               $this->currentRow = $this->fetchObject();
+               return $this->currentRow;
+       }
+
+       function valid() {
+               return $this->current() !== false;
+       }
+}
+
+class MySQLMasterPos {
+       var $file, $pos;
+
+       function __construct( $file, $pos ) {
+               $this->file = $file;
+               $this->pos = $pos;
+       }
+
+       function __toString() {
+               return "{$this->file}/{$this->pos}";
+       }
+}
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
new file mode 100755 (executable)
index 0000000..a6dda25
--- /dev/null
@@ -0,0 +1,1019 @@
+<?php
+/**
+ * This script is the MSSQL Server database abstraction layer
+ *
+ * See maintenance/mssql/README for development notes and other specific information
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabaseMssql extends Database {
+
+       var $mAffectedRows;
+       var $mLastResult;
+       var $mLastError;
+       var $mLastErrorNo;
+       var $mDatabaseFile;
+
+       /**
+        * Constructor
+        */
+       function __construct($server = false, $user = false, $password = false, $dbName = false,
+                       $failFunction = false, $flags = 0, $tablePrefix = 'get from global') {
+
+               global $wgOut, $wgDBprefix, $wgCommandLineMode;
+               if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet
+               $this->mOut =& $wgOut;
+               $this->mFailFunction = $failFunction;
+               $this->mFlags = $flags;
+
+               if ( $this->mFlags & DBO_DEFAULT ) {
+                       if ( $wgCommandLineMode ) {
+                               $this->mFlags &= ~DBO_TRX;
+                       } else {
+                               $this->mFlags |= DBO_TRX;
+                       }
+               }
+
+               /** Get the default table prefix*/
+               $this->mTablePrefix = $tablePrefix == 'get from global' ? $wgDBprefix : $tablePrefix;
+
+               if ($server) $this->open($server, $user, $password, $dbName);
+
+       }
+
+       /**
+        * todo: check if these should be true like parent class
+        */
+       function implicitGroupby()   { return false; }
+       function implicitOrderby()   { return false; }
+
+       static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
+               return new DatabaseMssql($server, $user, $password, $dbName, $failFunction, $flags);
+       }
+
+       /** Open an MSSQL database and return a resource handle to it
+        *  NOTE: only $dbName is used, the other parameters are irrelevant for MSSQL databases
+        */
+       function open($server,$user,$password,$dbName) {
+               wfProfileIn(__METHOD__);
+
+               # Test for missing mysql.so
+               # First try to load it
+               if (!@extension_loaded('mssql')) {
+                       @dl('mssql.so');
+               }
+
+               # Fail now
+               # Otherwise we get a suppressed fatal error, which is very hard to track down
+               if (!function_exists( 'mssql_connect')) {
+                       throw new DBConnectionError( $this, "MSSQL functions missing, have you compiled PHP with the --with-mssql option?\n" );
+               }
+
+               $this->close();
+               $this->mServer   = $server;
+               $this->mUser     = $user;
+               $this->mPassword = $password;
+               $this->mDBname   = $dbName;
+
+               wfProfileIn("dbconnect-$server");
+
+               # Try to connect up to three times
+               # The kernel's default SYN retransmission period is far too slow for us,
+               # so we use a short timeout plus a manual retry.
+               $this->mConn = false;
+               $max = 3;
+               for ( $i = 0; $i < $max && !$this->mConn; $i++ ) {
+                       if ( $i > 1 ) {
+                               usleep( 1000 );
+                       }
+                       if ($this->mFlags & DBO_PERSISTENT) {
+                               @/**/$this->mConn = mssql_pconnect($server, $user, $password);
+                       } else {
+                               # Create a new connection...
+                               @/**/$this->mConn = mssql_connect($server, $user, $password, true);
+                       }
+               }
+               
+               wfProfileOut("dbconnect-$server");
+
+               if ($dbName != '') {
+                       if ($this->mConn !== false) {
+                               $success = @/**/mssql_select_db($dbName, $this->mConn);
+                               if (!$success) {
+                                       $error = "Error selecting database $dbName on server {$this->mServer} " .
+                                               "from client host {$wguname['nodename']}\n";
+                                       wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+                                       wfDebug( $error );
+                               }
+                       } else {
+                               wfDebug("DB connection error\n");
+                               wfDebug("Server: $server, User: $user, Password: ".substr($password, 0, 3)."...\n");
+                               $success = false;
+                       }
+               } else {
+                       # Delay USE query
+                       $success = (bool)$this->mConn;
+               }
+
+               if (!$success) $this->reportConnectionError();
+               $this->mOpened = $success;
+               wfProfileOut(__METHOD__);
+               return $success;
+       }
+
+       /**
+        * Close an MSSQL database
+        */
+       function close() {
+               $this->mOpened = false;
+               if ($this->mConn) {
+                       if ($this->trxLevel()) $this->immediateCommit();
+                       return mssql_close($this->mConn);
+               } else return true;
+       }
+
+       /**
+        * - MSSQL doesn't seem to do buffered results
+        * - the trasnaction syntax is modified here to avoid having to replicate
+        *   Database::query which uses BEGIN, COMMIT, ROLLBACK
+        */
+       function doQuery($sql) {
+               if ($sql == 'BEGIN' || $sql == 'COMMIT' || $sql == 'ROLLBACK') return true; # $sql .= ' TRANSACTION';
+               $sql = preg_replace('|[^\x07-\x7e]|','?',$sql); # TODO: need to fix unicode - just removing it here while testing
+               $ret = mssql_query($sql, $this->mConn);
+               if ($ret === false) {
+                       $err = mssql_get_last_message();
+                       if ($err) $this->mlastError = $err;
+                       $row = mssql_fetch_row(mssql_query('select @@ERROR'));
+                       if ($row[0]) $this->mlastErrorNo = $row[0];
+               } else $this->mlastErrorNo = false;
+               return $ret;
+       }
+
+       /**#@+
+        * @param mixed $res A SQL result
+        */
+       /**
+        * Free a result object
+        */
+       function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               if ( !@/**/mssql_free_result( $res ) ) {
+                       throw new DBUnexpectedError( $this, "Unable to free MSSQL result" );
+               }
+       }
+
+       /**
+        * Fetch the next row from the given result object, in object form.
+        * Fields can be retrieved with $row->fieldname, with fields acting like
+        * member variables.
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
+        */
+       function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @/**/$row = mssql_fetch_object( $res );
+               if ( $this->lastErrno() ) {
+                       throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
+               }
+               return $row;
+       }
+
+       /**
+        * Fetch the next row from the given result object, in associative array
+        * form.  Fields are retrieved with $row['fieldname'].
+        *
+        * @param $res SQL result object as returned from Database::query(), etc.
+        * @return MySQL row object
+        * @throws DBUnexpectedError Thrown if the database returns an error
+        */
+       function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @/**/$row = mssql_fetch_array( $res );
+               if ( $this->lastErrno() ) {
+                       throw new DBUnexpectedError( $this, 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) );
+               }
+               return $row;
+       }
+
+       /**
+        * Get the number of rows in a result object
+        */
+       function numRows( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @/**/$n = mssql_num_rows( $res );
+               if ( $this->lastErrno() ) {
+                       throw new DBUnexpectedError( $this, 'Error in numRows(): ' . htmlspecialchars( $this->lastError() ) );
+               }
+               return $n;
+       }
+
+       /**
+        * Get the number of fields in a result object
+        * See documentation for mysql_num_fields()
+        */
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mssql_num_fields( $res );
+       }
+
+       /**
+        * Get a field name in a result object
+        * See documentation for mysql_field_name():
+        * http://www.php.net/mysql_field_name
+        */
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mssql_field_name( $res, $n );
+       }
+
+       /**
+        * Get the inserted value of an auto-increment row
+        *
+        * The value inserted should be fetched from nextSequenceValue()
+        *
+        * Example:
+        * $id = $dbw->nextSequenceValue('page_page_id_seq');
+        * $dbw->insert('page',array('page_id' => $id));
+        * $id = $dbw->insertId();
+        */
+       function insertId() {
+               $row = mssql_fetch_row(mssql_query('select @@IDENTITY'));
+               return $row[0];
+       }
+
+       /**
+        * Change the position of the cursor in a result object
+        * See mysql_data_seek()
+        */
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mssql_data_seek( $res, $row );
+       }
+
+       /**
+        * Get the last error number
+        */
+       function lastErrno() {
+               return $this->mlastErrorNo;
+       }
+
+       /**
+        * Get a description of the last error
+        */
+       function lastError() {
+               return $this->mlastError;
+       }
+
+       /**
+        * Get the number of rows affected by the last write query
+        */
+       function affectedRows() {
+               return mssql_rows_affected( $this->mConn );
+       }
+
+       /**
+        * Simple UPDATE wrapper
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns success
+        *
+        * This function exists for historical reasons, Database::update() has a more standard
+        * calling convention and feature set
+        */
+       function set( $table, $var, $value, $cond, $fname = 'Database::set' )
+       {
+               if ($value == "NULL") $value = "''"; # see comments in makeListWithoutNulls()
+               $table = $this->tableName( $table );
+               $sql = "UPDATE $table SET $var = '" .
+                 $this->strencode( $value ) . "' WHERE ($cond)";
+               return (bool)$this->query( $sql, $fname );
+       }
+
+       /**
+        * Simple SELECT wrapper, returns a single field, input must be encoded
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns FALSE on failure
+        */
+       function selectField( $table, $var, $cond='', $fname = 'Database::selectField', $options = array() ) {
+               if ( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               $options['LIMIT'] = 1;
+
+               $res = $this->select( $table, $var, $cond, $fname, $options );
+               if ( $res === false || !$this->numRows( $res ) ) {
+                       return false;
+               }
+               $row = $this->fetchRow( $res );
+               if ( $row !== false ) {
+                       $this->freeResult( $res );
+                       return $row[0];
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Returns an optional USE INDEX clause to go after the table, and a
+        * string to go at the end of the query
+        *
+        * @private
+        *
+        * @param array $options an associative array of options to be turned into
+        *              an SQL query, valid keys are listed in the function.
+        * @return array
+        */
+       function makeSelectOptions( $options ) {
+               $preLimitTail = $postLimitTail = '';
+               $startOpts = '';
+
+               $noKeyOptions = array();
+               foreach ( $options as $key => $option ) {
+                       if ( is_numeric( $key ) ) {
+                               $noKeyOptions[$option] = true;
+                       }
+               }
+
+               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+               
+               //if (isset($options['LIMIT'])) {
+               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
+               //              isset($options['OFFSET']) ? $options['OFFSET'] 
+               //              : false);
+               //}
+
+               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+               # Various MySQL extensions
+               if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) $startOpts .= ' /*! STRAIGHT_JOIN */';
+               if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) $startOpts .= ' HIGH_PRIORITY';
+               if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) $startOpts .= ' SQL_BIG_RESULT';
+               if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) $startOpts .= ' SQL_BUFFER_RESULT';
+               if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) $startOpts .= ' SQL_SMALL_RESULT';
+               if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) $startOpts .= ' SQL_CALC_FOUND_ROWS';
+               if ( isset( $noKeyOptions['SQL_CACHE'] ) ) $startOpts .= ' SQL_CACHE';
+               if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) $startOpts .= ' SQL_NO_CACHE';
+
+               if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+               } else {
+                       $useIndex = '';
+               }
+               
+               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+       }
+
+       /**
+        * SELECT wrapper
+        *
+        * @param mixed  $table   Array or string, table name(s) (prefix auto-added)
+        * @param mixed  $vars    Array or string, field name(s) to be retrieved
+        * @param mixed  $conds   Array or string, condition(s) for WHERE
+        * @param string $fname   Calling function name (use __METHOD__) for logs/profiling
+        * @param array  $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+        *                        see Database::makeSelectOptions code for list of supported stuff
+        * @return mixed Database result resource (feed to Database::fetchObject or whatever), or false on failure
+        */
+       function select( $table, $vars, $conds='', $fname = 'Database::select', $options = array() )
+       {
+               if( is_array( $vars ) ) {
+                       $vars = implode( ',', $vars );
+               }
+               if( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               if( is_array( $table ) ) {
+                       if ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
+                               $from = ' FROM ' . $this->tableNamesWithUseIndex( $table, $options['USE INDEX'] );
+                       else
+                               $from = ' FROM ' . implode( ',', array_map( array( &$this, 'tableName' ), $table ) );
+               } elseif ($table!='') {
+                       if ($table{0}==' ') {
+                               $from = ' FROM ' . $table;
+                       } else {
+                               $from = ' FROM ' . $this->tableName( $table );
+                       }
+               } else {
+                       $from = '';
+               }
+
+               list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) = $this->makeSelectOptions( $options );
+
+               if( !empty( $conds ) ) {
+                       if ( is_array( $conds ) ) {
+                               $conds = $this->makeList( $conds, LIST_AND );
+                       }
+                       $sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
+               } else {
+                       $sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
+               }
+
+               if (isset($options['LIMIT']))
+                       $sql = $this->limitResult($sql, $options['LIMIT'],
+                               isset($options['OFFSET']) ? $options['OFFSET'] : false);
+               $sql = "$sql $postLimitTail";
+               
+               if (isset($options['EXPLAIN'])) {
+                       $sql = 'EXPLAIN ' . $sql;
+               }
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * Takes same arguments as Database::select()
+        */
+       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+               $rows = 0;
+               $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options );
+               if ($res) {
+                       $row = $this->fetchObject($res);
+                       $rows = $row[0];
+               }
+               $this->freeResult($res);
+               return $rows;
+       }
+       
+       /**
+        * Determines whether a field exists in a table
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns NULL on failure
+        */
+       function fieldExists( $table, $field, $fname = 'Database::fieldExists' ) {
+               $table = $this->tableName( $table );
+               $sql = "SELECT TOP 1 * FROM $table";
+               $res = $this->query( $sql, 'Database::fieldExists' );
+
+               $found = false;
+               while ( $row = $this->fetchArray( $res ) ) {
+                       if ( isset($row[$field]) ) {
+                               $found = true;
+                               break;
+                       }
+               }
+
+               $this->freeResult( $res );
+               return $found;
+       }
+
+       /**
+        * Get information about an index into an object
+        * Returns false if the index does not exist
+        */
+       function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
+
+               throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
+               return NULL;
+
+               $table = $this->tableName( $table );
+               $sql = 'SHOW INDEX FROM '.$table;
+               $res = $this->query( $sql, $fname );
+               if ( !$res ) {
+                       return NULL;
+               }
+
+               $result = array();
+               while ( $row = $this->fetchObject( $res ) ) {
+                       if ( $row->Key_name == $index ) {
+                               $result[] = $row;
+                       }
+               }
+               $this->freeResult($res);
+               
+               return empty($result) ? false : $result;
+       }
+
+       /**
+        * Query whether a given table exists
+        */
+       function tableExists( $table ) {
+               $table = $this->tableName( $table );
+               $res = $this->query( "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '$table'" );
+               $exist = ($res->numRows() > 0);
+               $this->freeResult($res);
+               return $exist;
+       }
+
+       /**
+        * mysql_fetch_field() wrapper
+        * Returns false if the field doesn't exist
+        *
+        * @param $table
+        * @param $field
+        */
+       function fieldInfo( $table, $field ) {
+               $table = $this->tableName( $table );
+               $res = $this->query( "SELECT TOP 1 * FROM $table" );
+               $n = mssql_num_fields( $res->result );
+               for( $i = 0; $i < $n; $i++ ) {
+                       $meta = mssql_fetch_field( $res->result, $i );
+                       if( $field == $meta->name ) {
+                               return new MSSQLField($meta);
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * mysql_field_type() wrapper
+        */
+       function fieldType( $res, $index ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return mssql_field_type( $res, $index );
+       }
+
+       /**
+        * INSERT wrapper, inserts an array into a table
+        *
+        * $a may be a single associative array, or an array of these with numeric keys, for
+        * multi-row insert.
+        *
+        * Usually aborts on failure
+        * If errors are explicitly ignored, returns success
+        * 
+        * Same as parent class implementation except that it removes primary key from column lists
+        * because MSSQL doesn't support writing nulls to IDENTITY (AUTO_INCREMENT) columns
+        */
+       function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+               # No rows to insert, easy just return now
+               if ( !count( $a ) ) {
+                       return true;
+               }
+               $table = $this->tableName( $table );
+               if ( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               
+               # todo: need to record primary keys at table create time, and remove NULL assignments to them
+               if ( isset( $a[0] ) && is_array( $a[0] ) ) {
+                       $multi = true;
+                       $keys = array_keys( $a[0] );
+#                      if (ereg('_id$',$keys[0])) {
+                               foreach ($a as $i) {
+                                       if (is_null($i[$keys[0]])) unset($i[$keys[0]]); # remove primary-key column from multiple insert lists if empty value
+                               }
+#                      }
+                       $keys = array_keys( $a[0] );
+               } else {
+                       $multi = false;
+                       $keys = array_keys( $a );
+#                      if (ereg('_id$',$keys[0]) && empty($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
+                       if (is_null($a[$keys[0]])) unset($a[$keys[0]]); # remove primary-key column from insert list if empty value
+                       $keys = array_keys( $a );
+               }
+
+               # handle IGNORE option
+               # example:
+               #   MySQL: INSERT IGNORE INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
+               #   MSSQL: IF NOT EXISTS (SELECT * FROM user_groups WHERE ug_user = '1') INSERT INTO user_groups (ug_user,ug_group) VALUES ('1','sysop')
+               $ignore = in_array('IGNORE',$options);
+
+               # remove IGNORE from options list
+               if ($ignore) {
+                       $oldoptions = $options;
+                       $options = array();
+                       foreach ($oldoptions as $o) if ($o != 'IGNORE') $options[] = $o;
+               }
+
+               $keylist = implode(',', $keys);
+               $sql = 'INSERT '.implode(' ', $options)." INTO $table (".implode(',', $keys).') VALUES ';
+               if ($multi) {
+                       if ($ignore) {
+                               # If multiple and ignore, then do each row as a separate conditional insert
+                               foreach ($a as $row) {
+                                       $prival = $row[$keys[0]];
+                                       $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
+                                       if (!$this->query("$sql (".$this->makeListWithoutNulls($row).')', $fname)) return false;
+                               }
+                               return true;
+                       } else {
+                               $first = true;
+                               foreach ($a as $row) {
+                                       if ($first) $first = false; else $sql .= ',';
+                                       $sql .= '('.$this->makeListWithoutNulls($row).')';
+                               }
+                       }
+               } else {
+                       if ($ignore) {
+                               $prival = $a[$keys[0]];
+                               $sql = "IF NOT EXISTS (SELECT * FROM $table WHERE $keys[0] = '$prival') $sql";
+                       }
+                       $sql .= '('.$this->makeListWithoutNulls($a).')';
+               }
+               return (bool)$this->query( $sql, $fname );
+       }
+
+       /**
+        * MSSQL doesn't allow implicit casting of NULL's into non-null values for NOT NULL columns
+        *   for now I've just converted the NULL's in the lists for updates and inserts into empty strings
+        *   which get implicitly casted to 0 for numeric columns
+        * NOTE: the set() method above converts NULL to empty string as well but not via this method
+        */
+       function makeListWithoutNulls($a, $mode = LIST_COMMA) {
+               return str_replace("NULL","''",$this->makeList($a,$mode));
+       }
+
+       /**
+        * UPDATE wrapper, takes a condition array and a SET array
+        *
+        * @param string $table  The table to UPDATE
+        * @param array  $values An array of values to SET
+        * @param array  $conds  An array of conditions (WHERE). Use '*' to update all rows.
+        * @param string $fname  The Class::Function calling this function
+        *                       (for the log)
+        * @param array  $options An array of UPDATE options, can be one or
+        *                        more of IGNORE, LOW_PRIORITY
+        * @return bool
+        */
+       function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
+               $table = $this->tableName( $table );
+               $opts = $this->makeUpdateOptions( $options );
+               $sql = "UPDATE $opts $table SET " . $this->makeListWithoutNulls( $values, LIST_SET );
+               if ( $conds != '*' ) {
+                       $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
+               }
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Make UPDATE options for the Database::update function
+        *
+        * @private
+        * @param array $options The options passed to Database::update
+        * @return string
+        */
+       function makeUpdateOptions( $options ) {
+               if( !is_array( $options ) ) {
+                       $options = array( $options );
+               }
+               $opts = array();
+               if ( in_array( 'LOW_PRIORITY', $options ) )
+                       $opts[] = $this->lowPriorityOption();
+               if ( in_array( 'IGNORE', $options ) )
+                       $opts[] = 'IGNORE';
+               return implode(' ', $opts);
+       }
+
+       /**
+        * Change the current database
+        */
+       function selectDB( $db ) {
+               $this->mDBname = $db;
+               return mssql_select_db( $db, $this->mConn );
+       }
+
+       /**
+        * MSSQL has a problem with the backtick quoting, so all this does is ensure the prefix is added exactly once
+        */
+       function tableName($name) {
+               return strpos($name, $this->mTablePrefix) === 0 ? $name : "{$this->mTablePrefix}$name";
+       }
+
+       /**
+        * MSSQL doubles quotes instead of escaping them
+        * @param string $s String to be slashed.
+        * @return string slashed string.
+        */
+       function strencode($s) {
+               return str_replace("'","''",$s);
+       }
+
+       /**
+        * USE INDEX clause
+        */
+       function useIndexClause( $index ) {
+               return "";
+       }
+
+       /**
+        * REPLACE query wrapper
+        * PostgreSQL simulates this with a DELETE followed by INSERT
+        * $row is the row to insert, an associative array
+        * $uniqueIndexes is an array of indexes. Each element may be either a
+        * field name or an array of field names
+        *
+        * It may be more efficient to leave off unique indexes which are unlikely to collide.
+        * However if you do this, you run the risk of encountering errors which wouldn't have
+        * occurred in MySQL
+        *
+        * @todo migrate comment to phodocumentor format
+        */
+       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+               $table = $this->tableName( $table );
+
+               # Single row case
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = array( $rows );
+               }
+
+               $sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) .') VALUES ';
+               $first = true;
+               foreach ( $rows as $row ) {
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $sql .= ',';
+                       }
+                       $sql .= '(' . $this->makeList( $row ) . ')';
+               }
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * DELETE where the condition is a join
+        * MySQL does this with a multi-table DELETE syntax, PostgreSQL does it with sub-selects
+        *
+        * For safety, an empty $conds will not delete everything. If you want to delete all rows where the
+        * join condition matches, set $conds='*'
+        *
+        * DO NOT put the join condition in $conds
+        *
+        * @param string $delTable The table to delete from.
+        * @param string $joinTable The other table.
+        * @param string $delVar The variable to join on, in the first table.
+        * @param string $joinVar The variable to join on, in the second table.
+        * @param array $conds Condition array of field names mapped to variables, ANDed together in the WHERE clause
+        */
+       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = 'Database::deleteJoin' ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError( $this, 'Database::deleteJoin() called with empty $conds' );
+               }
+
+               $delTable = $this->tableName( $delTable );
+               $joinTable = $this->tableName( $joinTable );
+               $sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
+               if ( $conds != '*' ) {
+                       $sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
+               }
+
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Returns the size of a text field, or -1 for "unlimited"
+        */
+       function textFieldSize( $table, $field ) {
+               $table = $this->tableName( $table );
+               $sql = "SELECT TOP 1 * FROM $table;";
+               $res = $this->query( $sql, 'Database::textFieldSize' );
+               $row = $this->fetchObject( $res );
+               $this->freeResult( $res );
+
+               $m = array();
+               if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
+                       $size = $m[1];
+               } else {
+                       $size = -1;
+               }
+               return $size;
+       }
+
+       /**
+        * @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
+        */
+       function lowPriorityOption() {
+               return 'LOW_PRIORITY';
+       }
+
+       /**
+        * INSERT SELECT wrapper
+        * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+        * Source items may be literals rather than field names, but strings should be quoted with Database::addQuotes()
+        * $conds may be "*" to copy the whole table
+        * srcTable may be an array of tables.
+        */
+       function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'Database::insertSelect',
+               $insertOptions = array(), $selectOptions = array() )
+       {
+               $destTable = $this->tableName( $destTable );
+               if ( is_array( $insertOptions ) ) {
+                       $insertOptions = implode( ' ', $insertOptions );
+               }
+               if( !is_array( $selectOptions ) ) {
+                       $selectOptions = array( $selectOptions );
+               }
+               list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
+               if( is_array( $srcTable ) ) {
+                       $srcTable =  implode( ',', array_map( array( &$this, 'tableName' ), $srcTable ) );
+               } else {
+                       $srcTable = $this->tableName( $srcTable );
+               }
+               $sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+                       " SELECT $startOpts " . implode( ',', $varMap ) .
+                       " FROM $srcTable $useIndex ";
+               if ( $conds != '*' ) {
+                       $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+               }
+               $sql .= " $tailOpts";
+               return $this->query( $sql, $fname );
+       }
+
+       /**
+        * Construct a LIMIT query with optional offset
+        * This is used for query pages
+        * $sql string SQL query we will append the limit to
+        * $limit integer the SQL limit
+        * $offset integer the SQL offset (default false)
+        */
+       function limitResult($sql, $limit, $offset=false) {
+               if( !is_numeric($limit) ) {
+                       throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
+               }
+               if ($offset) {
+                       throw new DBUnexpectedError( $this, 'Database::limitResult called with non-zero offset which is not supported yet' );
+               } else {
+                       $sql = ereg_replace("^SELECT", "SELECT TOP $limit", $sql);
+               }
+               return $sql;
+       }
+
+       /**
+        * Returns an SQL expression for a simple conditional.
+        *
+        * @param string $cond SQL expression which will result in a boolean value
+        * @param string $trueVal SQL expression to return if true
+        * @param string $falseVal SQL expression to return if false
+        * @return string SQL fragment
+        */
+       function conditional( $cond, $trueVal, $falseVal ) {
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+       }
+
+       /**
+        * Should determine if the last failure was due to a deadlock
+        * - don't know how to do this in MSSQL
+        */
+       function wasDeadlock() {
+               return false;
+       }
+
+       /**
+        * Begin a transaction, committing any previously open transaction
+        * @deprecated use begin()
+        */
+       function immediateBegin( $fname = 'Database::immediateBegin' ) {
+               $this->begin();
+       }
+
+       /**
+        * Commit transaction, if one is open
+        * @deprecated use commit()
+        */
+       function immediateCommit( $fname = 'Database::immediateCommit' ) {
+               $this->commit();
+       }
+
+       /**
+        * Return MW-style timestamp used for MySQL schema
+        */
+       function timestamp( $ts=0 ) {
+               return wfTimestamp(TS_MW,$ts);
+       }
+
+       /**
+        * Local database timestamp format or null
+        */
+       function timestampOrNull( $ts = null ) {
+               if( is_null( $ts ) ) {
+                       return null;
+               } else {
+                       return $this->timestamp( $ts );
+               }
+       }
+
+       /**
+        * @return string wikitext of a link to the server software's web site
+        */
+       function getSoftwareLink() {
+               return "[http://www.microsoft.com/sql/default.mspx Microsoft SQL Server 2005 Home]";
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               $row = mssql_fetch_row(mssql_query('select @@VERSION'));
+               return ereg("^(.+[0-9]+\\.[0-9]+\\.[0-9]+) ",$row[0],$m) ? $m[1] : $row[0];
+       }
+
+       function limitResultForUpdate($sql, $num) {
+               return $sql;
+       }
+
+       /**
+        * not done
+        */
+       public function setTimeout($timeout) { return; }
+
+       function ping() {
+               wfDebug("Function ping() not written for MSSQL yet");
+               return true;
+       }
+
+       /**
+        * How lagged is this slave?
+        */
+       public function getLag() {
+               return 0;
+       }
+
+       /**
+        * Called by the installer script
+        * - this is the same way as DatabasePostgresql.php, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
+        */
+       public function setup_database() {
+               global $IP,$wgDBTableOptions;
+               $wgDBTableOptions = '';
+               $mysql_tmpl = "$IP/maintenance/tables.sql";
+               $mysql_iw   = "$IP/maintenance/interwiki.sql";
+               $mssql_tmpl = "$IP/maintenance/mssql/tables.sql";
+
+               # Make an MSSQL template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
+               if (!file_exists($mssql_tmpl)) { # todo: make this conditional again
+                       $sql = file_get_contents($mysql_tmpl);
+                       $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
+                       $sql = preg_replace('/^\s*(UNIQUE )?(INDEX|KEY|FULLTEXT).+?$/m', '', $sql); # These indexes should be created with a CREATE INDEX query
+                       $sql = preg_replace('/(\sKEY) [^\(]+\(/is', '$1 (', $sql); # "KEY foo (foo)" should just be "KEY (foo)"
+                       $sql = preg_replace('/(varchar\([0-9]+\))\s+binary/i', '$1', $sql); # "varchar(n) binary" cannot be followed by "binary"
+                       $sql = preg_replace('/(var)?binary\(([0-9]+)\)/ie', '"varchar(".strlen(pow(2,$2)).")"', $sql); # use varchar(chars) not binary(bits)
+                       $sql = preg_replace('/ (var)?binary/i', ' varchar', $sql); # use varchar not binary
+                       $sql = preg_replace('/(varchar\([0-9]+\)(?! N))/', '$1 NULL', $sql); # MSSQL complains if NULL is put into a varchar
+                       #$sql = preg_replace('/ binary/i',' varchar',$sql); # MSSQL binary's can't be assigned with strings, so use varchar's instead
+                       #$sql = preg_replace('/(binary\([0-9]+\) (NOT NULL )?default) [\'"].*?[\'"]/i','$1 0',$sql); # binary default cannot be string
+                       $sql = preg_replace('/[a-z]*(blob|text)([ ,])/i', 'text$2', $sql); # no BLOB types in MSSQL
+                       $sql = preg_replace('/\).+?;/',');', $sql); # remove all table options
+                       $sql = preg_replace('/ (un)?signed/i', '', $sql);
+                       $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
+                       $sql = str_replace(' bool ', ' bit ', $sql);
+                       $sql = str_replace('auto_increment', 'IDENTITY(1,1)', $sql);
+                       #$sql = preg_replace('/NOT NULL(?! IDENTITY)/', 'NULL', $sql); # Allow NULL's for non IDENTITY columns
+
+                       # Tidy up and write file
+                       $sql = preg_replace('/,\s*\)/s', "\n)", $sql); # Remove spurious commas left after INDEX removals
+                       $sql = preg_replace('/^\s*^/m', '', $sql); # Remove empty lines
+                       $sql = preg_replace('/;$/m', ";\n", $sql); # Separate each statement with an empty line
+                       file_put_contents($mssql_tmpl, $sql);
+               }
+
+               # Parse the MSSQL template replacing inline variables such as /*$wgDBprefix*/
+               $err = $this->sourceFile($mssql_tmpl);
+               if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+
+               # Use DatabasePostgres's code to populate interwiki from MySQL template
+               $f = fopen($mysql_iw,'r');
+               if ($f == false) dieout("<li>Could not find the interwiki.sql file");
+               $sql = "INSERT INTO {$this->mTablePrefix}interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+               while (!feof($f)) {
+                       $line = fgets($f,1024);
+                       $matches = array();
+                       if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
+                       $this->query("$sql $matches[1],$matches[2])");
+               }
+       }
+
+}
+
+/**
+ * @ingroup Database
+ */
+class MSSQLField extends MySQLField {
+
+       function __construct() {
+       }
+
+       static function fromText($db, $table, $field) {
+               $n = new MSSQLField;
+               $n->name = $field;
+               $n->tablename = $table;
+               return $n;
+       }
+
+} // end DatabaseMssql class
+
diff --git a/includes/db/DatabaseOracle.php b/includes/db/DatabaseOracle.php
new file mode 100644 (file)
index 0000000..8d2a675
--- /dev/null
@@ -0,0 +1,710 @@
+<?php
+/**
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * This is the Oracle database abstraction layer.
+ * @ingroup Database
+ */
+class ORABlob {
+       var $mData;
+
+       function __construct($data) {
+               $this->mData = $data;
+       }
+
+       function getData() {
+               return $this->mData;
+       }
+}
+
+/**
+ * The oci8 extension is fairly weak and doesn't support oci_num_rows, among
+ * other things.  We use a wrapper class to handle that and other
+ * Oracle-specific bits, like converting column names back to lowercase.
+ * @ingroup Database
+ */
+class ORAResult {
+       private $rows;
+       private $cursor;
+       private $stmt;
+       private $nrows;
+       private $db;
+
+       function __construct(&$db, $stmt) {
+               $this->db =& $db;
+               if (($this->nrows = oci_fetch_all($stmt, $this->rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW | OCI_NUM)) === false) {
+                       $e = oci_error($stmt);
+                       $db->reportQueryError($e['message'], $e['code'], '', __FUNCTION__);
+                       return;
+               }
+
+               $this->cursor = 0;
+               $this->stmt = $stmt;
+       }
+
+       function free() {
+               oci_free_statement($this->stmt);
+       }
+
+       function seek($row) {
+               $this->cursor = min($row, $this->nrows);
+       }
+
+       function numRows() {
+               return $this->nrows;
+       }
+
+       function numFields() {
+               return oci_num_fields($this->stmt);
+       }
+
+       function fetchObject() {
+               if ($this->cursor >= $this->nrows)
+                       return false;
+
+               $row = $this->rows[$this->cursor++];
+               $ret = new stdClass();
+               foreach ($row as $k => $v) {
+                       $lc = strtolower(oci_field_name($this->stmt, $k + 1));
+                       $ret->$lc = $v;
+               }
+
+               return $ret;
+       }
+
+       function fetchAssoc() {
+               if ($this->cursor >= $this->nrows)
+                       return false;
+
+               $row = $this->rows[$this->cursor++];
+               $ret = array();
+               foreach ($row as $k => $v) {
+                       $lc = strtolower(oci_field_name($this->stmt, $k + 1));
+                       $ret[$lc] = $v;
+                       $ret[$k] = $v;
+               }
+               return $ret;
+       }
+}
+
+/**
+ * @ingroup Database
+ */
+class DatabaseOracle extends Database {
+       var $mInsertId = NULL;
+       var $mLastResult = NULL;
+       var $numeric_version = NULL;
+       var $lastResult = null;
+       var $cursor = 0;
+       var $mAffectedRows;
+
+       function DatabaseOracle($server = false, $user = false, $password = false, $dbName = false,
+               $failFunction = false, $flags = 0 )
+       {
+
+               global $wgOut;
+               # Can't get a reference if it hasn't been set yet
+               if ( !isset( $wgOut ) ) {
+                       $wgOut = NULL;
+               }
+               $this->mOut =& $wgOut;
+               $this->mFailFunction = $failFunction;
+               $this->mFlags = $flags;
+               $this->open( $server, $user, $password, $dbName);
+
+       }
+
+       function cascadingDeletes() {
+               return true;
+       }
+       function cleanupTriggers() {
+               return true;
+       }
+       function strictIPs() {
+               return true;
+       }
+       function realTimestamps() {
+               return true;
+       }
+       function implicitGroupby() {
+               return false;
+       }
+       function implicitOrderby() {
+               return false;
+       }
+       function searchableIPs() {
+               return true;
+       }
+
+       static function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
+               $failFunction = false, $flags = 0)
+       {
+               return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
+       }
+
+       /**
+        * Usually aborts on failure
+        * If the failFunction is set to a non-zero integer, returns success
+        */
+       function open( $server, $user, $password, $dbName ) {
+               if ( !function_exists( 'oci_connect' ) ) {
+                       throw new DBConnectionError( $this, "Oracle functions missing, have you compiled PHP with the --with-oci8 option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
+               }
+
+               # Needed for proper UTF-8 functionality
+               putenv("NLS_LANG=AMERICAN_AMERICA.AL32UTF8");
+
+               $this->close();
+               $this->mServer = $server;
+               $this->mUser = $user;
+               $this->mPassword = $password;
+               $this->mDBname = $dbName;
+
+               if (!strlen($user)) { ## e.g. the class is being loaded
+                       return;
+               }
+
+               error_reporting( E_ALL );
+               $this->mConn = oci_connect($user, $password, $dbName);
+
+               if ($this->mConn == false) {
+                       wfDebug("DB connection error\n");
+                       wfDebug("Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n");
+                       wfDebug($this->lastError()."\n");
+                       return false;
+               }
+
+               $this->mOpened = true;
+               return $this->mConn;
+       }
+
+       /**
+        * Closes a database connection, if it is open
+        * Returns success, true if already closed
+        */
+       function close() {
+               $this->mOpened = false;
+               if ( $this->mConn ) {
+                       return oci_close( $this->mConn );
+               } else {
+                       return true;
+               }
+       }
+
+       function execFlags() {
+               return $this->mTrxLevel ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS;
+       }
+
+       function doQuery($sql) {
+               wfDebug("SQL: [$sql]\n");
+               if (!mb_check_encoding($sql)) {
+                       throw new MWException("SQL encoding is invalid");
+               }
+
+               if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) {
+                       $e = oci_error($this->mConn);
+                       $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+               }
+
+               if (oci_execute($stmt, $this->execFlags()) == false) {
+                       $e = oci_error($stmt);
+                       $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+               }
+               if (oci_statement_type($stmt) == "SELECT")
+                       return new ORAResult($this, $stmt);
+               else {
+                       $this->mAffectedRows = oci_num_rows($stmt);
+                       return true;
+               }
+       }
+
+       function queryIgnore($sql, $fname = '') {
+               return $this->query($sql, $fname, true);
+       }
+
+       function freeResult($res) {
+               $res->free();
+       }
+
+       function fetchObject($res) {
+               return $res->fetchObject();
+       }
+
+       function fetchRow($res) {
+               return $res->fetchAssoc();
+       }
+
+       function numRows($res) {
+               return $res->numRows();
+       }
+
+       function numFields($res) {
+               return $res->numFields();
+       }
+
+       function fieldName($stmt, $n) {
+               return pg_field_name($stmt, $n);
+       }
+
+       /**
+        * This must be called after nextSequenceVal
+        */
+       function insertId() {
+               return $this->mInsertId;
+       }
+
+       function dataSeek($res, $row) {
+               $res->seek($row);
+       }
+
+       function lastError() {
+               if ($this->mConn === false)
+                       $e = oci_error();
+               else
+                       $e = oci_error($this->mConn);
+               return $e['message'];
+       }
+
+       function lastErrno() {
+               if ($this->mConn === false)
+                       $e = oci_error();
+               else
+                       $e = oci_error($this->mConn);
+               return $e['code'];
+       }
+
+       function affectedRows() {
+               return $this->mAffectedRows;
+       }
+
+       /**
+        * Returns information about an index
+        * If errors are explicitly ignored, returns NULL on failure
+        */
+       function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+               return false;
+       }
+
+       function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+               return false;
+       }
+
+       function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
+               if (!is_array($options))
+                       $options = array($options);
+
+               #if (in_array('IGNORE', $options))
+               #       $oldIgnore = $this->ignoreErrors(true);
+
+               # IGNORE is performed using single-row inserts, ignoring errors in each
+               # FIXME: need some way to distiguish between key collision and other types of error
+               //$oldIgnore = $this->ignoreErrors(true);
+               if (!is_array(reset($a))) {
+                       $a = array($a);
+               }
+               foreach ($a as $row) {
+                       $this->insertOneRow($table, $row, $fname);
+               }
+               //$this->ignoreErrors($oldIgnore);
+               $retVal = true;
+
+               //if (in_array('IGNORE', $options))
+               //      $this->ignoreErrors($oldIgnore);
+
+               return $retVal;
+       }
+
+       function insertOneRow($table, $row, $fname) {
+               // "INSERT INTO tables (a, b, c)"
+               $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')';
+               $sql .= " VALUES (";
+
+               // for each value, append ":key"
+               $first = true;
+               $returning = '';
+               foreach ($row as $col => $val) {
+                       if (is_object($val)) {
+                               $what = "EMPTY_BLOB()";
+                               assert($returning === '');
+                               $returning = " RETURNING $col INTO :bval";
+                               $blobcol = $col;
+                       } else
+                               $what = ":$col";
+
+                       if ($first)
+                               $sql .= "$what";
+                       else
+                               $sql.= ", $what";
+                       $first = false;
+               }
+               $sql .= ") $returning";
+
+               $stmt = oci_parse($this->mConn, $sql);
+               foreach ($row as $col => $val) {
+                       if (!is_object($val)) {
+                               if (oci_bind_by_name($stmt, ":$col", $row[$col]) === false)
+                                       $this->reportQueryError($this->lastErrno(), $this->lastError(), $sql, __METHOD__);
+                       }
+               }
+
+               if (($bval = oci_new_descriptor($this->mConn, OCI_D_LOB)) === false) {
+                       $e = oci_error($stmt);
+                       throw new DBUnexpectedError($this, "Cannot create LOB descriptor: " . $e['message']);
+               }
+
+               if (strlen($returning))
+                       oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB);
+
+               if (oci_execute($stmt, OCI_DEFAULT) === false) {
+                       $e = oci_error($stmt);
+                       $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__);
+               }
+               if (strlen($returning)) {
+                       $bval->save($row[$blobcol]->getData());
+                       $bval->free();
+               }
+               if (!$this->mTrxLevel)
+                       oci_commit($this->mConn);
+
+               oci_free_statement($stmt);
+       }
+
+       function tableName( $name ) {
+               # Replace reserved words with better ones
+               switch( $name ) {
+                       case 'user':
+                               return 'mwuser';
+                       case 'text':
+                               return 'pagecontent';
+                       default:
+                               return $name;
+               }
+       }
+
+       /**
+        * Return the next in a sequence, save the value for retrieval via insertId()
+        */
+       function nextSequenceValue($seqName) {
+               $res = $this->query("SELECT $seqName.nextval FROM dual");
+               $row = $this->fetchRow($res);
+               $this->mInsertId = $row[0];
+               $this->freeResult($res);
+               return $this->mInsertId;
+       }
+
+       /**
+        * Oracle does not have a "USE INDEX" clause, so return an empty string
+        */
+       function useIndexClause($index) {
+               return '';
+       }
+
+       # REPLACE query wrapper
+       # Oracle simulates this with a DELETE followed by INSERT
+       # $row is the row to insert, an associative array
+       # $uniqueIndexes is an array of indexes. Each element may be either a
+       # field name or an array of field names
+       #
+       # It may be more efficient to leave off unique indexes which are unlikely to collide.
+       # However if you do this, you run the risk of encountering errors which wouldn't have
+       # occurred in MySQL
+       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+               $table = $this->tableName($table);
+
+               if (count($rows)==0) {
+                       return;
+               }
+
+               # Single row case
+               if (!is_array(reset($rows))) {
+                       $rows = array($rows);
+               }
+
+               foreach( $rows as $row ) {
+                       # Delete rows which collide
+                       if ( $uniqueIndexes ) {
+                               $sql = "DELETE FROM $table WHERE ";
+                               $first = true;
+                               foreach ( $uniqueIndexes as $index ) {
+                                       if ( $first ) {
+                                               $first = false;
+                                               $sql .= "(";
+                                       } else {
+                                               $sql .= ') OR (';
+                                       }
+                                       if ( is_array( $index ) ) {
+                                               $first2 = true;
+                                               foreach ( $index as $col ) {
+                                                       if ( $first2 ) {
+                                                               $first2 = false;
+                                                       } else {
+                                                               $sql .= ' AND ';
+                                                       }
+                                                       $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+                                               }
+                                       } else {
+                                               $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+                                       }
+                               }
+                               $sql .= ')';
+                               $this->query( $sql, $fname );
+                       }
+
+                       # Now insert the row
+                       $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+                               $this->makeList( $row, LIST_COMMA ) . ')';
+                       $this->query($sql, $fname);
+               }
+       }
+
+       # DELETE where the condition is a join
+       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError($this,  'Database::deleteJoin() called with empty $conds' );
+               }
+
+               $delTable = $this->tableName( $delTable );
+               $joinTable = $this->tableName( $joinTable );
+               $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+               if ( $conds != '*' ) {
+                       $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+               }
+               $sql .= ')';
+
+               $this->query( $sql, $fname );
+       }
+
+       # Returns the size of a text field, or -1 for "unlimited"
+       function textFieldSize( $table, $field ) {
+               $table = $this->tableName( $table );
+               $sql = "SELECT t.typname as ftype,a.atttypmod as size
+                       FROM pg_class c, pg_attribute a, pg_type t
+                       WHERE relname='$table' AND a.attrelid=c.oid AND
+                               a.atttypid=t.oid and a.attname='$field'";
+               $res =$this->query($sql);
+               $row=$this->fetchObject($res);
+               if ($row->ftype=="varchar") {
+                       $size=$row->size-4;
+               } else {
+                       $size=$row->size;
+               }
+               $this->freeResult( $res );
+               return $size;
+       }
+
+       function lowPriorityOption() {
+               return '';
+       }
+
+       function limitResult($sql, $limit, $offset) {
+               if ($offset === false)
+                       $offset = 0;
+               return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset";
+       }
+
+       /**
+        * Returns an SQL expression for a simple conditional.
+        * Uses CASE on Oracle
+        *
+        * @param string $cond SQL expression which will result in a boolean value
+        * @param string $trueVal SQL expression to return if true
+        * @param string $falseVal SQL expression to return if false
+        * @return string SQL fragment
+        */
+       function conditional( $cond, $trueVal, $falseVal ) {
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+       }
+
+       function wasDeadlock() {
+               return $this->lastErrno() == 'OCI-00060';
+       }
+
+       function timestamp($ts = 0) {
+               return wfTimestamp(TS_ORACLE, $ts);
+       }
+
+       /**
+        * Return aggregated value function call
+        */
+       function aggregateValue ($valuedata,$valuename='value') {
+               return $valuedata;
+       }
+
+       function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) {
+               # Ignore errors during error handling to avoid infinite
+               # recursion
+               $ignore = $this->ignoreErrors(true);
+               ++$this->mErrorCount;
+
+               if ($ignore || $tempIgnore) {
+echo "error ignored! query = [$sql]\n";
+                       wfDebug("SQL ERROR (ignored): $error\n");
+                       $this->ignoreErrors( $ignore );
+               }
+               else {
+echo "error!\n";
+                       $message = "A database error has occurred\n" .
+                               "Query: $sql\n" .
+                               "Function: $fname\n" .
+                               "Error: $errno $error\n";
+                       throw new DBUnexpectedError($this, $message);
+               }
+       }
+
+       /**
+        * @return string wikitext of a link to the server software's web site
+        */
+       function getSoftwareLink() {
+               return "[http://www.oracle.com/ Oracle]";
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               return oci_server_version($this->mConn);
+       }
+
+       /**
+        * Query whether a given table exists (in the given schema, or the default mw one if not given)
+        */
+       function tableExists($table) {
+               $etable= $this->addQuotes($table);
+               $SQL = "SELECT 1 FROM user_tables WHERE table_name='$etable'";
+               $res = $this->query($SQL);
+               $count = $res ? oci_num_rows($res) : 0;
+               if ($res)
+                       $this->freeResult($res);
+               return $count;
+       }
+
+       /**
+        * Query whether a given column exists in the mediawiki schema
+        */
+       function fieldExists( $table, $field ) {
+               return true; // XXX
+       }
+
+       function fieldInfo( $table, $field ) {
+               return false; // XXX
+       }
+
+       function begin( $fname = '' ) {
+               $this->mTrxLevel = 1;
+       }
+       function immediateCommit( $fname = '' ) {
+               return true;
+       }
+       function commit( $fname = '' ) {
+               oci_commit($this->mConn);
+               $this->mTrxLevel = 0;
+       }
+
+       /* Not even sure why this is used in the main codebase... */
+       function limitResultForUpdate($sql, $num) {
+               return $sql;
+       }
+
+       function strencode($s) {
+               return str_replace("'", "''", $s);
+       }
+
+       function encodeBlob($b) {
+               return new ORABlob($b);
+       }
+       function decodeBlob($b) {
+               return $b; //return $b->load();
+       }
+
+       function addQuotes( $s ) {
+       global  $wgLang;
+               $s = $wgLang->checkTitleEncoding($s);
+               return "'" . $this->strencode($s) . "'";
+       }
+
+       function quote_ident( $s ) {
+               return $s;
+       }
+
+       /* For now, does nothing */
+       function selectDB( $db ) {
+               return true;
+       }
+
+       /**
+        * Returns an optional USE INDEX clause to go after the table, and a
+        * string to go at the end of the query
+        *
+        * @private
+        *
+        * @param array $options an associative array of options to be turned into
+        *              an SQL query, valid keys are listed in the function.
+        * @return array
+        */
+       function makeSelectOptions( $options ) {
+               $preLimitTail = $postLimitTail = '';
+               $startOpts = '';
+
+               $noKeyOptions = array();
+               foreach ( $options as $key => $option ) {
+                       if ( is_numeric( $key ) ) {
+                               $noKeyOptions[$option] = true;
+                       }
+               }
+
+               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
+
+               if (isset($options['LIMIT'])) {
+               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
+               //              isset($options['OFFSET']) ? $options['OFFSET']
+               //              : false);
+               }
+
+               #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
+               #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+               if ( isset( $options['USE INDEX'] ) && ! is_array( $options['USE INDEX'] ) ) {
+                       $useIndex = $this->useIndexClause( $options['USE INDEX'] );
+               } else {
+                       $useIndex = '';
+               }
+
+               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+       }
+
+       public function setTimeout( $timeout ) {
+               // @todo fixme no-op
+       }
+
+       function ping() {
+               wfDebug( "Function ping() not written for DatabaseOracle.php yet");
+               return true;
+       }
+
+       /**
+        * How lagged is this slave?
+        *
+        * @return int
+        */
+       public function getLag() {
+               # Not implemented for Oracle
+               return 0;
+       }
+
+       function setFakeSlaveLag() {}
+       function setFakeMaster() {}
+
+       function getDBname() {
+               return $this->mDBname;
+       }
+
+       function getServer() {
+               return $this->mServer;
+       }
+
+} // end DatabaseOracle class
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
new file mode 100644 (file)
index 0000000..8f8488b
--- /dev/null
@@ -0,0 +1,1330 @@
+<?php
+/**
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * This is the Postgres database abstraction layer.
+ *
+ * As it includes more generic version for DB functions,
+ * than MySQL ones, some of them should be moved to parent
+ * Database class.
+ *
+ * @ingroup Database
+ */
+class PostgresField {
+       private $name, $tablename, $type, $nullable, $max_length;
+
+       static function fromText($db, $table, $field) {
+       global $wgDBmwschema;
+
+               $q = <<<END
+SELECT
+CASE WHEN typname = 'int2' THEN 'smallint'
+WHEN typname = 'int4' THEN 'integer'
+WHEN typname = 'int8' THEN 'bigint'
+WHEN typname = 'bpchar' THEN 'char'
+ELSE typname END AS typname,
+attnotnull, attlen
+FROM pg_class, pg_namespace, pg_attribute, pg_type
+WHERE relnamespace=pg_namespace.oid
+AND relkind='r'
+AND attrelid=pg_class.oid
+AND atttypid=pg_type.oid
+AND nspname=%s
+AND relname=%s
+AND attname=%s;
+END;
+               $res = $db->query(sprintf($q,
+                               $db->addQuotes($wgDBmwschema),
+                               $db->addQuotes($table),
+                               $db->addQuotes($field)));
+               $row = $db->fetchObject($res);
+               if (!$row)
+                       return null;
+               $n = new PostgresField;
+               $n->type = $row->typname;
+               $n->nullable = ($row->attnotnull == 'f');
+               $n->name = $field;
+               $n->tablename = $table;
+               $n->max_length = $row->attlen;
+               return $n;
+       }
+
+       function name() {
+               return $this->name;
+       }
+
+       function tableName() {
+               return $this->tablename;
+       }
+
+       function type() {
+               return $this->type;
+       }
+
+       function nullable() {
+               return $this->nullable;
+       }
+
+       function maxLength() {
+               return $this->max_length;
+       }
+}
+
+/**
+ * @ingroup Database
+ */
+class DatabasePostgres extends Database {
+       var $mInsertId = NULL;
+       var $mLastResult = NULL;
+       var $numeric_version = NULL;
+
+       function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
+               $failFunction = false, $flags = 0 )
+       {
+
+               global $wgOut;
+               # Can't get a reference if it hasn't been set yet
+               if ( !isset( $wgOut ) ) {
+                       $wgOut = NULL;
+               }
+               $this->mOut =& $wgOut;
+               $this->mFailFunction = $failFunction;
+               $this->mFlags = $flags;
+               $this->open( $server, $user, $password, $dbName);
+
+       }
+
+       function cascadingDeletes() {
+               return true;
+       }
+       function cleanupTriggers() {
+               return true;
+       }
+       function strictIPs() {
+               return true;
+       }
+       function realTimestamps() {
+               return true;
+       }
+       function implicitGroupby() {
+               return false;
+       }
+       function implicitOrderby() {
+               return false;
+       }
+       function searchableIPs() {
+               return true;
+       }
+       function functionalIndexes() {
+               return true;
+       }
+
+       function hasConstraint( $name ) {
+               global $wgDBmwschema;
+               $SQL = "SELECT 1 FROM pg_catalog.pg_constraint c, pg_catalog.pg_namespace n WHERE c.connamespace = n.oid AND conname = '" . pg_escape_string( $name ) . "' AND n.nspname = '" . pg_escape_string($wgDBmwschema) ."'";
+               return $this->numRows($res = $this->doQuery($SQL));
+       }
+
+       static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
+       {
+               return new DatabasePostgres( $server, $user, $password, $dbName, $failFunction, $flags );
+       }
+
+       /**
+        * Usually aborts on failure
+        * If the failFunction is set to a non-zero integer, returns success
+        */
+       function open( $server, $user, $password, $dbName ) {
+               # Test for Postgres support, to avoid suppressed fatal error
+               if ( !function_exists( 'pg_connect' ) ) {
+                       throw new DBConnectionError( $this, "Postgres functions missing, have you compiled PHP with the --with-pgsql option?\n (Note: if you recently installed PHP, you may need to restart your webserver and database)\n" );
+               }
+
+               global $wgDBport;
+
+               if (!strlen($user)) { ## e.g. the class is being loaded
+                       return;
+               }
+
+               $this->close();
+               $this->mServer = $server;
+               $this->mPort = $port = $wgDBport;
+               $this->mUser = $user;
+               $this->mPassword = $password;
+               $this->mDBname = $dbName;
+
+               $hstring="";
+               if ($server!=false && $server!="") {
+                       $hstring="host=$server ";
+               }
+               if ($port!=false && $port!="") {
+                       $hstring .= "port=$port ";
+               }
+
+               error_reporting( E_ALL );
+               @$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
+
+               if ( $this->mConn == false ) {
+                       wfDebug( "DB connection error\n" );
+                       wfDebug( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
+                       wfDebug( $this->lastError()."\n" );
+                       return false;
+               }
+
+               $this->mOpened = true;
+
+               global $wgCommandLineMode;
+               ## If called from the command-line (e.g. importDump), only show errors
+               if ($wgCommandLineMode) {
+                       $this->doQuery("SET client_min_messages = 'ERROR'");
+               }
+
+               global $wgDBmwschema, $wgDBts2schema;
+               if (isset( $wgDBmwschema ) && isset( $wgDBts2schema )
+                       && $wgDBmwschema !== 'mediawiki'
+                       && preg_match( '/^\w+$/', $wgDBmwschema )
+                       && preg_match( '/^\w+$/', $wgDBts2schema )
+               ) {
+                       $safeschema = $this->quote_ident($wgDBmwschema);
+                       $safeschema2 = $this->quote_ident($wgDBts2schema);
+                       $this->doQuery("SET search_path = $safeschema, $wgDBts2schema, public");
+               }
+
+               return $this->mConn;
+       }
+
+
+       function initial_setup($password, $dbName) {
+               // If this is the initial connection, setup the schema stuff and possibly create the user
+               global $wgDBname, $wgDBuser, $wgDBpassword, $wgDBsuperuser, $wgDBmwschema, $wgDBts2schema;
+
+               print "<li>Checking the version of Postgres...";
+               $version = $this->getServerVersion();
+               $PGMINVER = '8.1';
+               if ($this->numeric_version < $PGMINVER) {
+                       print "<b>FAILED</b>. Required version is $PGMINVER. You have $this->numeric_version ($version)</li>\n";
+                       dieout("</ul>");
+               }
+               print "version $this->numeric_version is OK.</li>\n";
+
+               $safeuser = $this->quote_ident($wgDBuser);
+               // Are we connecting as a superuser for the first time?
+               if ($wgDBsuperuser) {
+                       // Are we really a superuser? Check out our rights
+                       $SQL = "SELECT
+                      CASE WHEN usesuper IS TRUE THEN
+                      CASE WHEN usecreatedb IS TRUE THEN 3 ELSE 1 END
+                      ELSE CASE WHEN usecreatedb IS TRUE THEN 2 ELSE 0 END
+                    END AS rights
+                    FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBsuperuser);
+                       $rows = $this->numRows($res = $this->doQuery($SQL));
+                       if (!$rows) {
+                               print "<li>ERROR: Could not read permissions for user \"$wgDBsuperuser\"</li>\n";
+                               dieout('</ul>');
+                       }
+                       $perms = pg_fetch_result($res, 0, 0);
+
+                       $SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser);
+                       $rows = $this->numRows($this->doQuery($SQL));
+                       if ($rows) {
+                               print "<li>User \"$wgDBuser\" already exists, skipping account creation.</li>";
+                       }
+                       else {
+                               if ($perms != 1 and $perms != 3) {
+                                       print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. ";
+                                       print 'Please use a different Postgres user.</li>';
+                                       dieout('</ul>');
+                               }
+                               print "<li>Creating user <b>$wgDBuser</b>...";
+                               $safepass = $this->addQuotes($wgDBpassword);
+                               $SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass";
+                               $this->doQuery($SQL);
+                               print "OK</li>\n";
+                       }
+                       // User now exists, check out the database
+                       if ($dbName != $wgDBname) {
+                               $SQL = "SELECT 1 FROM pg_catalog.pg_database WHERE datname = " . $this->addQuotes($wgDBname);
+                               $rows = $this->numRows($this->doQuery($SQL));
+                               if ($rows) {
+                                       print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>";
+                               }
+                               else {
+                                       if ($perms < 2) {
+                                               print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create databases. ";
+                                               print 'Please use a different Postgres user.</li>';
+                                               dieout('</ul>');
+                                       }
+                                       print "<li>Creating database <b>$wgDBname</b>...";
+                                       $safename = $this->quote_ident($wgDBname);
+                                       $SQL = "CREATE DATABASE $safename OWNER $safeuser ";
+                                       $this->doQuery($SQL);
+                                       print "OK</li>\n";
+                                       // Hopefully tsearch2 and plpgsql are in template1...
+                               }
+
+                               // Reconnect to check out tsearch2 rights for this user
+                               print "<li>Connecting to \"$wgDBname\" as superuser \"$wgDBsuperuser\" to check rights...";
+
+                               $hstring="";
+                               if ($this->mServer!=false && $this->mServer!="") {
+                                       $hstring="host=$this->mServer ";
+                               }
+                               if ($this->mPort!=false && $this->mPort!="") {
+                                       $hstring .= "port=$this->mPort ";
+                               }
+
+                               @$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$wgDBsuperuser password=$password");
+                               if ( $this->mConn == false ) {
+                                       print "<b>FAILED TO CONNECT!</b></li>";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>\n";
+                       }
+
+                       if ($this->numeric_version < 8.3) {
+                               // Tsearch2 checks
+                               print "<li>Checking that tsearch2 is installed in the database \"$wgDBname\"...";
+                               if (! $this->tableExists("pg_ts_cfg", $wgDBts2schema)) {
+                                       print "<b>FAILED</b>. tsearch2 must be installed in the database \"$wgDBname\".";
+                                       print "Please see <a href='http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
+                                       print " for instructions or ask on #postgresql on irc.freenode.net</li>\n";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>\n";
+                               print "<li>Ensuring that user \"$wgDBuser\" has select rights on the tsearch2 tables...";
+                               foreach (array('cfg','cfgmap','dict','parser') as $table) {
+                                       $SQL = "GRANT SELECT ON pg_ts_$table TO $safeuser";
+                                       $this->doQuery($SQL);
+                               }
+                               print "OK</li>\n";
+                       }
+
+                       // Setup the schema for this user if needed
+                       $result = $this->schemaExists($wgDBmwschema);
+                       $safeschema = $this->quote_ident($wgDBmwschema);
+                       if (!$result) {
+                               print "<li>Creating schema <b>$wgDBmwschema</b> ...";
+                               $result = $this->doQuery("CREATE SCHEMA $safeschema AUTHORIZATION $safeuser");
+                               if (!$result) {
+                                       print "<b>FAILED</b>.</li>\n";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>\n";
+                       }
+                       else {
+                               print "<li>Schema already exists, explicitly granting rights...\n";
+                               $safeschema2 = $this->addQuotes($wgDBmwschema);
+                               $SQL = "SELECT 'GRANT ALL ON '||pg_catalog.quote_ident(relname)||' TO $safeuser;'\n".
+                                       "FROM pg_catalog.pg_class p, pg_catalog.pg_namespace n\n".
+                                       "WHERE relnamespace = n.oid AND n.nspname = $safeschema2\n".
+                                       "AND p.relkind IN ('r','S','v')\n";
+                               $SQL .= "UNION\n";
+                               $SQL .= "SELECT 'GRANT ALL ON FUNCTION '||pg_catalog.quote_ident(proname)||'('||\n".
+                                       "pg_catalog.oidvectortypes(p.proargtypes)||') TO $safeuser;'\n".
+                                       "FROM pg_catalog.pg_proc p, pg_catalog.pg_namespace n\n".
+                                       "WHERE p.pronamespace = n.oid AND n.nspname = $safeschema2";
+                               $res = $this->doQuery($SQL);
+                               if (!$res) {
+                                       print "<b>FAILED</b>. Could not set rights for the user.</li>\n";
+                                       dieout("</ul>");
+                               }
+                               $this->doQuery("SET search_path = $safeschema");
+                               $rows = $this->numRows($res);
+                               while ($rows) {
+                                       $rows--;
+                                       $this->doQuery(pg_fetch_result($res, $rows, 0));
+                               }
+                               print "OK</li>";
+                       }
+
+                       // Install plpgsql if needed
+                       $this->setup_plpgsql();
+
+                       $wgDBsuperuser = '';
+                       return true; // Reconnect as regular user
+
+               } // end superuser
+
+               if (!defined('POSTGRES_SEARCHPATH')) {
+
+                       if ($this->numeric_version < 8.3) {
+                               // Do we have the basic tsearch2 table?
+                               print "<li>Checking for tsearch2 in the schema \"$wgDBts2schema\"...";
+                               if (! $this->tableExists("pg_ts_dict", $wgDBts2schema)) {
+                                       print "<b>FAILED</b>. Make sure tsearch2 is installed. See <a href=";
+                                       print "'http://www.devx.com/opensource/Article/21674/0/page/2'>this article</a>";
+                                       print " for instructions.</li>\n";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>\n";
+
+                               // Does this user have the rights to the tsearch2 tables?
+                               $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
+                               print "<li>Checking tsearch2 permissions...";
+                               // Let's check all four, just to be safe
+                               error_reporting( 0 );
+                               $ts2tables = array('cfg','cfgmap','dict','parser');
+                               $safetsschema = $this->quote_ident($wgDBts2schema);
+                               foreach ( $ts2tables AS $tname ) {
+                                       $SQL = "SELECT count(*) FROM $safetsschema.pg_ts_$tname";
+                                       $res = $this->doQuery($SQL);
+                                       if (!$res) {
+                                               print "<b>FAILED</b> to access pg_ts_$tname. Make sure that the user ".
+                                                       "\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n";
+                                               dieout("</ul>");
+                                       }
+                               }
+                               $SQL = "SELECT ts_name FROM $safetsschema.pg_ts_cfg WHERE locale = '$ctype'";
+                               $SQL .= " ORDER BY CASE WHEN ts_name <> 'default' THEN 1 ELSE 0 END";
+                               $res = $this->doQuery($SQL);
+                               error_reporting( E_ALL );
+                               if (!$res) {
+                                       print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>";
+
+                               // Will the current locale work? Can we force it to?
+                               print "<li>Verifying tsearch2 locale with $ctype...";
+                               $rows = $this->numRows($res);
+                               $resetlocale = 0;
+                               if (!$rows) {
+                                       print "<b>not found</b></li>\n";
+                                       print "<li>Attempting to set default tsearch2 locale to \"$ctype\"...";
+                                       $resetlocale = 1;
+                               }
+                               else {
+                                       $tsname = pg_fetch_result($res, 0, 0);
+                                       if ($tsname != 'default') {
+                                               print "<b>not set to default ($tsname)</b>";
+                                               print "<li>Attempting to change tsearch2 default locale to \"$ctype\"...";
+                                               $resetlocale = 1;
+                                       }
+                               }
+                               if ($resetlocale) {
+                                       $SQL = "UPDATE $safetsschema.pg_ts_cfg SET locale = '$ctype' WHERE ts_name = 'default'";
+                                       $res = $this->doQuery($SQL);
+                                       if (!$res) {
+                                               print "<b>FAILED</b>. ";
+                                               print "Please make sure that the locale in pg_ts_cfg for \"default\" is set to \"$ctype\"</li>\n";
+                                               dieout("</ul>");
+                                       }
+                                       print "OK</li>";
+                               }
+
+                               // Final test: try out a simple tsearch2 query
+                               $SQL = "SELECT $safetsschema.to_tsvector('default','MediaWiki tsearch2 testing')";
+                               $res = $this->doQuery($SQL);
+                               if (!$res) {
+                                       print "<b>FAILED</b>. Specifically, \"$SQL\" did not work.</li>";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>";
+                       }
+
+                       // Install plpgsql if needed
+                       $this->setup_plpgsql();
+
+                       // Does the schema already exist? Who owns it?
+                       $result = $this->schemaExists($wgDBmwschema);
+                       if (!$result) {
+                               print "<li>Creating schema <b>$wgDBmwschema</b> ...";
+                               error_reporting( 0 );
+                               $safeschema = $this->quote_ident($wgDBmwschema);
+                               $result = $this->doQuery("CREATE SCHEMA $safeschema");
+                               error_reporting( E_ALL );
+                               if (!$result) {
+                                       print "<b>FAILED</b>. The user \"$wgDBuser\" must be able to access the schema. ".
+                                               "You can try making them the owner of the database, or try creating the schema with a ".
+                                               "different user, and then grant access to the \"$wgDBuser\" user.</li>\n";
+                                       dieout("</ul>");
+                               }
+                               print "OK</li>\n";
+                       }
+                       else if ($result != $wgDBuser) {
+                               print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$wgDBuser\". Not ideal.</li>\n";
+                       }
+                       else {
+                               print "<li>Schema \"$wgDBmwschema\" exists and is owned by \"$wgDBuser\". Excellent.</li>\n";
+                       }
+
+                       // Always return GMT time to accomodate the existing integer-based timestamp assumption
+                       print "<li>Setting the timezone to GMT for user \"$wgDBuser\" ...";
+                       $SQL = "ALTER USER $safeuser SET timezone = 'GMT'";
+                       $result = pg_query($this->mConn, $SQL);
+                       if (!$result) {
+                               print "<b>FAILED</b>.</li>\n";
+                               dieout("</ul>");
+                       }
+                       print "OK</li>\n";
+                       // Set for the rest of this session
+                       $SQL = "SET timezone = 'GMT'";
+                       $result = pg_query($this->mConn, $SQL);
+                       if (!$result) {
+                               print "<li>Failed to set timezone</li>\n";
+                               dieout("</ul>");
+                       }
+
+                       print "<li>Setting the datestyle to ISO, YMD for user \"$wgDBuser\" ...";
+                       $SQL = "ALTER USER $safeuser SET datestyle = 'ISO, YMD'";
+                       $result = pg_query($this->mConn, $SQL);
+                       if (!$result) {
+                               print "<b>FAILED</b>.</li>\n";
+                               dieout("</ul>");
+                       }
+                       print "OK</li>\n";
+                       // Set for the rest of this session
+                       $SQL = "SET datestyle = 'ISO, YMD'";
+                       $result = pg_query($this->mConn, $SQL);
+                       if (!$result) {
+                               print "<li>Failed to set datestyle</li>\n";
+                               dieout("</ul>");
+                       }
+
+                       // Fix up the search paths if needed
+                       print "<li>Setting the search path for user \"$wgDBuser\" ...";
+                       $path = $this->quote_ident($wgDBmwschema);
+                       if ($wgDBts2schema !== $wgDBmwschema)
+                               $path .= ", ". $this->quote_ident($wgDBts2schema);
+                       if ($wgDBmwschema !== 'public' and $wgDBts2schema !== 'public')
+                               $path .= ", public";
+                       $SQL = "ALTER USER $safeuser SET search_path = $path";
+                       $result = pg_query($this->mConn, $SQL);
+                       if (!$result) {
+                               print "<b>FAILED</b>.</li>\n";
+                               dieout("</ul>");
+                       }
+                       print "OK</li>\n";
+                       // Set for the rest of this session
+                       $SQL = "SET search_path = $path";
+                       $result = pg_query($this->mConn, $SQL);
+                       if (!$result) {
+                               print "<li>Failed to set search_path</li>\n";
+                               dieout("</ul>");
+                       }
+                       define( "POSTGRES_SEARCHPATH", $path );
+               }
+       }
+
+
+       function setup_plpgsql() {
+               print "<li>Checking for Pl/Pgsql ...";
+               $SQL = "SELECT 1 FROM pg_catalog.pg_language WHERE lanname = 'plpgsql'";
+               $rows = $this->numRows($this->doQuery($SQL));
+               if ($rows < 1) {
+                       // plpgsql is not installed, but if we have a pg_pltemplate table, we should be able to create it
+                       print "not installed. Attempting to install Pl/Pgsql ...";
+                       $SQL = "SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) ".
+                               "WHERE relname = 'pg_pltemplate' AND nspname='pg_catalog'";
+                       $rows = $this->numRows($this->doQuery($SQL));
+                       if ($rows >= 1) {
+                       $olde = error_reporting(0);
+                               error_reporting($olde - E_WARNING);
+                               $result = $this->doQuery("CREATE LANGUAGE plpgsql");
+                               error_reporting($olde);
+                               if (!$result) {
+                                       print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
+                                       dieout("</ul>");
+                               }
+                       }
+                       else {
+                               print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></li>";
+                               dieout("</ul>");
+                       }
+               }
+               print "OK</li>\n";
+       }
+
+
+       /**
+        * Closes a database connection, if it is open
+        * Returns success, true if already closed
+        */
+       function close() {
+               $this->mOpened = false;
+               if ( $this->mConn ) {
+                       return pg_close( $this->mConn );
+               } else {
+                       return true;
+               }
+       }
+
+       function doQuery( $sql ) {
+               if (function_exists('mb_convert_encoding')) {
+                       return $this->mLastResult=pg_query( $this->mConn , mb_convert_encoding($sql,'UTF-8') );
+               }
+               return $this->mLastResult=pg_query( $this->mConn , $sql);
+       }
+
+       function queryIgnore( $sql, $fname = '' ) {
+               return $this->query( $sql, $fname, true );
+       }
+
+       function freeResult( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               if ( !@pg_free_result( $res ) ) {
+                       throw new DBUnexpectedError($this,  "Unable to free Postgres result\n" );
+               }
+       }
+
+       function fetchObject( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @$row = pg_fetch_object( $res );
+               # FIXME: HACK HACK HACK HACK debug
+
+               # TODO:
+               # hashar : not sure if the following test really trigger if the object
+               #          fetching failed.
+               if( pg_last_error($this->mConn) ) {
+                       throw new DBUnexpectedError($this,  'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+               }
+               return $row;
+       }
+
+       function fetchRow( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @$row = pg_fetch_array( $res );
+               if( pg_last_error($this->mConn) ) {
+                       throw new DBUnexpectedError($this,  'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+               }
+               return $row;
+       }
+
+       function numRows( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               @$n = pg_num_rows( $res );
+               if( pg_last_error($this->mConn) ) {
+                       throw new DBUnexpectedError($this,  'SQL error: ' . htmlspecialchars( pg_last_error($this->mConn) ) );
+               }
+               return $n;
+       }
+       function numFields( $res ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return pg_num_fields( $res );
+       }
+       function fieldName( $res, $n ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return pg_field_name( $res, $n );
+       }
+
+       /**
+        * This must be called after nextSequenceVal
+        */
+       function insertId() {
+               return $this->mInsertId;
+       }
+
+       function dataSeek( $res, $row ) {
+               if ( $res instanceof ResultWrapper ) {
+                       $res = $res->result;
+               }
+               return pg_result_seek( $res, $row );
+       }
+
+       function lastError() {
+               if ( $this->mConn ) {
+                       return pg_last_error();
+               }
+               else {
+                       return "No database connection";
+               }
+       }
+       function lastErrno() {
+               return pg_last_error() ? 1 : 0;
+       }
+
+       function affectedRows() {
+               if( !isset( $this->mLastResult ) or ! $this->mLastResult )
+                       return 0;
+
+               return pg_affected_rows( $this->mLastResult );
+       }
+
+       /**
+        * Estimate rows in dataset
+        * Returns estimated count, based on EXPLAIN output
+        * This is not necessarily an accurate estimate, so use sparingly
+        * Returns -1 if count cannot be found
+        * Takes same arguments as Database::select()
+        */
+
+       function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+               $options['EXPLAIN'] = true;
+               $res = $this->select( $table, $vars, $conds, $fname, $options );
+               $rows = -1;
+               if ( $res ) {
+                       $row = $this->fetchRow( $res );
+                       $count = array();
+                       if( preg_match( '/rows=(\d+)/', $row[0], $count ) ) {
+                               $rows = $count[1];
+                       }
+                       $this->freeResult($res);
+               }
+               return $rows;
+       }
+
+
+       /**
+        * Returns information about an index
+        * If errors are explicitly ignored, returns NULL on failure
+        */
+       function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
+               $res = $this->query( $sql, $fname );
+               if ( !$res ) {
+                       return NULL;
+               }
+               while ( $row = $this->fetchObject( $res ) ) {
+                       if ( $row->indexname == $index ) {
+                               return $row;
+                       }
+               }
+               return false;
+       }
+
+       function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+               $sql = "SELECT indexname FROM pg_indexes WHERE tablename='{$table}'".
+                       " AND indexdef LIKE 'CREATE UNIQUE%({$index})'";
+               $res = $this->query( $sql, $fname );
+               if ( !$res )
+                       return NULL;
+               while ($row = $this->fetchObject( $res ))
+                       return true;
+               return false;
+
+       }
+
+       /**
+        * INSERT wrapper, inserts an array into a table
+        *
+        * $args may be a single associative array, or an array of these with numeric keys,
+        * for multi-row insert (Postgres version 8.2 and above only).
+        *
+        * @param array $table   String: Name of the table to insert to.
+        * @param array $args    Array: Items to insert into the table.
+        * @param array $fname   String: Name of the function, for profiling
+        * @param mixed $options String or Array. Valid options: IGNORE
+        *
+        * @return bool Success of insert operation. IGNORE always returns true.
+        */
+       function insert( $table, $args, $fname = 'DatabasePostgres::insert', $options = array() ) {
+               global $wgDBversion;
+
+               if ( !count( $args ) ) {
+                       return true;
+               }
+
+               $table = $this->tableName( $table );
+               if (! isset( $wgDBversion ) ) {
+                       $this->getServerVersion();
+                       $wgDBversion = $this->numeric_version;
+               }
+
+               if ( !is_array( $options ) )
+                       $options = array( $options );
+
+               if ( isset( $args[0] ) && is_array( $args[0] ) ) {
+                       $multi = true;
+                       $keys = array_keys( $args[0] );
+               }
+               else {
+                       $multi = false;
+                       $keys = array_keys( $args );
+               }
+
+               $ignore = in_array( 'IGNORE', $options ) ? 1 : 0;
+               if ( $ignore )
+                       $olde = error_reporting( 0 );
+
+               $sql = "INSERT INTO $table (" . implode( ',', $keys ) . ') VALUES ';
+
+               if ( $multi ) {
+                       if ( $wgDBversion >= 8.2 ) {
+                               $first = true;
+                               foreach ( $args as $row ) {
+                                       if ( $first ) {
+                                               $first = false;
+                                       } else {
+                                               $sql .= ',';
+                                       }
+                                       $sql .= '(' . $this->makeList( $row ) . ')';
+                               }
+                               $res = (bool)$this->query( $sql, $fname, $ignore );
+                       }
+                       else {
+                               $res = true;
+                               $origsql = $sql;
+                               foreach ( $args as $row ) {
+                                       $tempsql = $origsql;
+                                       $tempsql .= '(' . $this->makeList( $row ) . ')';
+                                       $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
+                                       if (! $tempres)
+                                               $res = false;
+                               }
+                       }
+               }
+               else {
+                       $sql .= '(' . $this->makeList( $args ) . ')';
+                       $res = (bool)$this->query( $sql, $fname, $ignore );
+               }
+
+               if ( $ignore ) {
+                       $olde = error_reporting( $olde );
+                       return true;
+               }
+
+               return $res;
+
+       }
+
+       function tableName( $name ) {
+               # Replace reserved words with better ones
+               switch( $name ) {
+                       case 'user':
+                               return 'mwuser';
+                       case 'text':
+                               return 'pagecontent';
+                       default:
+                               return $name;
+               }
+       }
+
+       /**
+        * Return the next in a sequence, save the value for retrieval via insertId()
+        */
+       function nextSequenceValue( $seqName ) {
+               $safeseq = preg_replace( "/'/", "''", $seqName );
+               $res = $this->query( "SELECT nextval('$safeseq')" );
+               $row = $this->fetchRow( $res );
+               $this->mInsertId = $row[0];
+               $this->freeResult( $res );
+               return $this->mInsertId;
+       }
+
+       /**
+        * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
+        */
+       function currentSequenceValue( $seqName ) {
+               $safeseq = preg_replace( "/'/", "''", $seqName );
+               $res = $this->query( "SELECT currval('$safeseq')" );
+               $row = $this->fetchRow( $res );
+               $currval = $row[0];
+               $this->freeResult( $res );
+               return $currval;
+       }
+
+       /**
+        * Postgres does not have a "USE INDEX" clause, so return an empty string
+        */
+       function useIndexClause( $index ) {
+               return '';
+       }
+
+       # REPLACE query wrapper
+       # Postgres simulates this with a DELETE followed by INSERT
+       # $row is the row to insert, an associative array
+       # $uniqueIndexes is an array of indexes. Each element may be either a
+       # field name or an array of field names
+       #
+       # It may be more efficient to leave off unique indexes which are unlikely to collide.
+       # However if you do this, you run the risk of encountering errors which wouldn't have
+       # occurred in MySQL
+       function replace( $table, $uniqueIndexes, $rows, $fname = 'Database::replace' ) {
+               $table = $this->tableName( $table );
+
+               if (count($rows)==0) {
+                       return;
+               }
+
+               # Single row case
+               if ( !is_array( reset( $rows ) ) ) {
+                       $rows = array( $rows );
+               }
+
+               foreach( $rows as $row ) {
+                       # Delete rows which collide
+                       if ( $uniqueIndexes ) {
+                               $sql = "DELETE FROM $table WHERE ";
+                               $first = true;
+                               foreach ( $uniqueIndexes as $index ) {
+                                       if ( $first ) {
+                                               $first = false;
+                                               $sql .= "(";
+                                       } else {
+                                               $sql .= ') OR (';
+                                       }
+                                       if ( is_array( $index ) ) {
+                                               $first2 = true;
+                                               foreach ( $index as $col ) {
+                                                       if ( $first2 ) {
+                                                               $first2 = false;
+                                                       } else {
+                                                               $sql .= ' AND ';
+                                                       }
+                                                       $sql .= $col.'=' . $this->addQuotes( $row[$col] );
+                                               }
+                                       } else {
+                                               $sql .= $index.'=' . $this->addQuotes( $row[$index] );
+                                       }
+                               }
+                               $sql .= ')';
+                               $this->query( $sql, $fname );
+                       }
+
+                       # Now insert the row
+                       $sql = "INSERT INTO $table (" . $this->makeList( array_keys( $row ), LIST_NAMES ) .') VALUES (' .
+                               $this->makeList( $row, LIST_COMMA ) . ')';
+                       $this->query( $sql, $fname );
+               }
+       }
+
+       # DELETE where the condition is a join
+       function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+               if ( !$conds ) {
+                       throw new DBUnexpectedError($this,  'Database::deleteJoin() called with empty $conds' );
+               }
+
+               $delTable = $this->tableName( $delTable );
+               $joinTable = $this->tableName( $joinTable );
+               $sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
+               if ( $conds != '*' ) {
+                       $sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
+               }
+               $sql .= ')';
+
+               $this->query( $sql, $fname );
+       }
+
+       # Returns the size of a text field, or -1 for "unlimited"
+       function textFieldSize( $table, $field ) {
+               $table = $this->tableName( $table );
+               $sql = "SELECT t.typname as ftype,a.atttypmod as size
+                       FROM pg_class c, pg_attribute a, pg_type t
+                       WHERE relname='$table' AND a.attrelid=c.oid AND
+                               a.atttypid=t.oid and a.attname='$field'";
+               $res =$this->query($sql);
+               $row=$this->fetchObject($res);
+               if ($row->ftype=="varchar") {
+                       $size=$row->size-4;
+               } else {
+                       $size=$row->size;
+               }
+               $this->freeResult( $res );
+               return $size;
+       }
+
+       function lowPriorityOption() {
+               return '';
+       }
+
+       function limitResult($sql, $limit, $offset=false) {
+               return "$sql LIMIT $limit ".(is_numeric($offset)?" OFFSET {$offset} ":"");
+       }
+
+       /**
+        * Returns an SQL expression for a simple conditional.
+        * Uses CASE on Postgres
+        *
+        * @param string $cond SQL expression which will result in a boolean value
+        * @param string $trueVal SQL expression to return if true
+        * @param string $falseVal SQL expression to return if false
+        * @return string SQL fragment
+        */
+       function conditional( $cond, $trueVal, $falseVal ) {
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+       }
+
+       function wasDeadlock() {
+               return $this->lastErrno() == '40P01';
+       }
+
+       function timestamp( $ts=0 ) {
+               return wfTimestamp(TS_POSTGRES,$ts);
+       }
+
+       /**
+        * Return aggregated value function call
+        */
+       function aggregateValue ($valuedata,$valuename='value') {
+               return $valuedata;
+       }
+
+
+       function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
+               // Ignore errors during error handling to avoid infinite recursion
+               $ignore = $this->ignoreErrors( true );
+               $this->mErrorCount++;
+
+               if ($ignore || $tempIgnore) {
+                       wfDebug("SQL ERROR (ignored): $error\n");
+                       $this->ignoreErrors( $ignore );
+               }
+               else {
+                       $message = "A database error has occurred\n" .
+                               "Query: $sql\n" .
+                               "Function: $fname\n" .
+                               "Error: $errno $error\n";
+                       throw new DBUnexpectedError($this, $message);
+               }
+       }
+
+       /**
+        * @return string wikitext of a link to the server software's web site
+        */
+               function getSoftwareLink() {
+               return "[http://www.postgresql.org/ PostgreSQL]";
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               $version = pg_fetch_result($this->doQuery("SELECT version()"),0,0);
+               $thisver = array();
+               if (!preg_match('/PostgreSQL (\d+\.\d+)(\S+)/', $version, $thisver)) {
+                       die("Could not determine the numeric version from $version!");
+               }
+               $this->numeric_version = $thisver[1];
+               return $version;
+       }
+
+
+       /**
+        * Query whether a given relation exists (in the given schema, or the
+        * default mw one if not given)
+        */
+       function relationExists( $table, $types, $schema = false ) {
+               global $wgDBmwschema;
+               if (!is_array($types))
+                       $types = array($types);
+               if (! $schema )
+                       $schema = $wgDBmwschema;
+               $etable = $this->addQuotes($table);
+               $eschema = $this->addQuotes($schema);
+               $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n "
+                       . "WHERE c.relnamespace = n.oid AND c.relname = $etable AND n.nspname = $eschema "
+                       . "AND c.relkind IN ('" . implode("','", $types) . "')";
+               $res = $this->query( $SQL );
+               $count = $res ? $res->numRows() : 0;
+               if ($res)
+                       $this->freeResult( $res );
+               return $count ? true : false;
+       }
+
+       /*
+        * For backward compatibility, this function checks both tables and
+        * views.
+        */
+       function tableExists ($table, $schema = false) {
+               return $this->relationExists($table, array('r', 'v'), $schema);
+       }
+
+       function sequenceExists ($sequence, $schema = false) {
+               return $this->relationExists($sequence, 'S', $schema);
+       }
+
+       function triggerExists($table, $trigger) {
+               global $wgDBmwschema;
+
+               $q = <<<END
+       SELECT 1 FROM pg_class, pg_namespace, pg_trigger
+               WHERE relnamespace=pg_namespace.oid AND relkind='r'
+                     AND tgrelid=pg_class.oid
+                     AND nspname=%s AND relname=%s AND tgname=%s
+END;
+               $res = $this->query(sprintf($q,
+                               $this->addQuotes($wgDBmwschema),
+                               $this->addQuotes($table),
+                               $this->addQuotes($trigger)));
+               if (!$res)
+                       return NULL;
+               $rows = $res->numRows();
+               $this->freeResult($res);
+               return $rows;
+       }
+
+       function ruleExists($table, $rule) {
+               global $wgDBmwschema;
+               $exists = $this->selectField("pg_rules", "rulename",
+                               array(  "rulename" => $rule,
+                                       "tablename" => $table,
+                                       "schemaname" => $wgDBmwschema));
+               return $exists === $rule;
+       }
+
+       function constraintExists($table, $constraint) {
+               global $wgDBmwschema;
+               $SQL = sprintf("SELECT 1 FROM information_schema.table_constraints ".
+                          "WHERE constraint_schema = %s AND table_name = %s AND constraint_name = %s",
+                       $this->addQuotes($wgDBmwschema),
+                       $this->addQuotes($table),
+                       $this->addQuotes($constraint));
+               $res = $this->query($SQL);
+               if (!$res)
+                       return NULL;
+               $rows = $res->numRows();
+               $this->freeResult($res);
+               return $rows;
+       }
+
+       /**
+        * Query whether a given schema exists. Returns the name of the owner
+        */
+       function schemaExists( $schema ) {
+               $eschema = preg_replace("/'/", "''", $schema);
+               $SQL = "SELECT rolname FROM pg_catalog.pg_namespace n, pg_catalog.pg_roles r "
+                               ."WHERE n.nspowner=r.oid AND n.nspname = '$eschema'";
+               $res = $this->query( $SQL );
+               if ( $res && $res->numRows() ) {
+                       $row = $res->fetchObject();
+                       $owner = $row->rolname;
+               } else {
+                       $owner = false;
+               }
+               if ($res)
+                       $this->freeResult($res);
+               return $owner;
+       }
+
+       /**
+        * Query whether a given column exists in the mediawiki schema
+        */
+       function fieldExists( $table, $field, $fname = 'DatabasePostgres::fieldExists' ) {
+               global $wgDBmwschema;
+               $etable = preg_replace("/'/", "''", $table);
+               $eschema = preg_replace("/'/", "''", $wgDBmwschema);
+               $ecol = preg_replace("/'/", "''", $field);
+               $SQL = "SELECT 1 FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n, pg_catalog.pg_attribute a "
+                       . "WHERE c.relnamespace = n.oid AND c.relname = '$etable' AND n.nspname = '$eschema' "
+                       . "AND a.attrelid = c.oid AND a.attname = '$ecol'";
+               $res = $this->query( $SQL, $fname );
+               $count = $res ? $res->numRows() : 0;
+               if ($res)
+                       $this->freeResult( $res );
+               return $count;
+       }
+
+       function fieldInfo( $table, $field ) {
+               return PostgresField::fromText($this, $table, $field);
+       }
+
+       function begin( $fname = 'DatabasePostgres::begin' ) {
+               $this->query( 'BEGIN', $fname );
+               $this->mTrxLevel = 1;
+       }
+       function immediateCommit( $fname = 'DatabasePostgres::immediateCommit' ) {
+               return true;
+       }
+       function commit( $fname = 'DatabasePostgres::commit' ) {
+               $this->query( 'COMMIT', $fname );
+               $this->mTrxLevel = 0;
+       }
+
+       /* Not even sure why this is used in the main codebase... */
+       function limitResultForUpdate($sql, $num) {
+               return $sql;
+       }
+
+       function setup_database() {
+               global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
+
+               // Make sure that we can write to the correct schema
+               // If not, Postgres will happily and silently go to the next search_path item
+               $ctest = "mediawiki_test_table";
+               $safeschema = $this->quote_ident($wgDBmwschema);
+               if ($this->tableExists($ctest, $wgDBmwschema)) {
+                       $this->doQuery("DROP TABLE $safeschema.$ctest");
+               }
+               $SQL = "CREATE TABLE $safeschema.$ctest(a int)";
+               $olde = error_reporting( 0 );
+               $res = $this->doQuery($SQL);
+               error_reporting( $olde );
+               if (!$res) {
+                       print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n";
+                       dieout("</ul>");
+               }
+               $this->doQuery("DROP TABLE $safeschema.$ctest");
+
+               $res = dbsource( "../maintenance/postgres/tables.sql", $this);
+
+               ## Update version information
+               $mwv = $this->addQuotes($wgVersion);
+               $pgv = $this->addQuotes($this->getServerVersion());
+               $pgu = $this->addQuotes($this->mUser);
+               $mws = $this->addQuotes($wgDBmwschema);
+               $tss = $this->addQuotes($wgDBts2schema);
+               $pgp = $this->addQuotes($wgDBport);
+               $dbn = $this->addQuotes($this->mDBname);
+               $ctype = pg_fetch_result($this->doQuery("SHOW lc_ctype"),0,0);
+
+               $SQL = "UPDATE mediawiki_version SET mw_version=$mwv, pg_version=$pgv, pg_user=$pgu, ".
+                               "mw_schema = $mws, ts2_schema = $tss, pg_port=$pgp, pg_dbname=$dbn, ".
+                               "ctype = '$ctype' ".
+                               "WHERE type = 'Creation'";
+               $this->query($SQL);
+
+               ## Avoid the non-standard "REPLACE INTO" syntax
+               $f = fopen( "../maintenance/interwiki.sql", 'r' );
+               if ($f == false ) {
+                       dieout( "<li>Could not find the interwiki.sql file");
+               }
+               ## We simply assume it is already empty as we have just created it
+               $SQL = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+               while ( ! feof( $f ) ) {
+                       $line = fgets($f,1024);
+                       $matches = array();
+                       if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) {
+                               continue;
+                       }
+                       $this->query("$SQL $matches[1],$matches[2])");
+               }
+               print " (table interwiki successfully populated)...\n";
+
+               $this->doQuery("COMMIT");
+       }
+
+       function encodeBlob( $b ) {
+               return new Blob ( pg_escape_bytea( $b ) ) ;
+       }
+
+       function decodeBlob( $b ) {
+               if ($b instanceof Blob) {
+                       $b = $b->fetch();
+               }
+               return pg_unescape_bytea( $b );
+       }
+
+       function strencode( $s ) { ## Should not be called by us
+               return pg_escape_string( $s );
+       }
+
+       function addQuotes( $s ) {
+               if ( is_null( $s ) ) {
+                       return 'NULL';
+               } else if ($s instanceof Blob) {
+                       return "'".$s->fetch($s)."'";
+               }
+               return "'" . pg_escape_string($s) . "'";
+       }
+
+       function quote_ident( $s ) {
+               return '"' . preg_replace( '/"/', '""', $s) . '"';
+       }
+
+       /* For now, does nothing */
+       function selectDB( $db ) {
+               return true;
+       }
+
+       /**
+        * Postgres specific version of replaceVars.
+        * Calls the parent version in Database.php
+        *
+        * @private
+        *
+        * @param string $com SQL string, read from a stream (usually tables.sql)
+        *
+        * @return string SQL string
+        */
+       protected function replaceVars( $ins ) {
+
+               $ins = parent::replaceVars( $ins );
+
+               if ($this->numeric_version >= 8.3) {
+                       // Thanks for not providing backwards-compatibility, 8.3
+                       $ins = preg_replace( "/to_tsvector\s*\(\s*'default'\s*,/", 'to_tsvector(', $ins );
+               }
+
+               if ($this->numeric_version <= 8.1) { // Our minimum version
+                       $ins = str_replace( 'USING gin', 'USING gist', $ins );
+               }
+
+               return $ins;
+       }
+
+       /**
+        * Various select options
+        *
+        * @private
+        *
+        * @param array $options an associative array of options to be turned into
+        *              an SQL query, valid keys are listed in the function.
+        * @return array
+        */
+       function makeSelectOptions( $options ) {
+               $preLimitTail = $postLimitTail = '';
+               $startOpts = $useIndex = '';
+
+               $noKeyOptions = array();
+               foreach ( $options as $key => $option ) {
+                       if ( is_numeric( $key ) ) {
+                               $noKeyOptions[$option] = true;
+                       }
+               }
+
+               if ( isset( $options['GROUP BY'] ) ) $preLimitTail .= " GROUP BY " . $options['GROUP BY'];
+               if ( isset( $options['HAVING'] ) ) $preLimitTail .= " HAVING {$options['HAVING']}";
+               if ( isset( $options['ORDER BY'] ) ) $preLimitTail .= " ORDER BY " . $options['ORDER BY'];
+
+               //if (isset($options['LIMIT'])) {
+               //      $tailOpts .= $this->limitResult('', $options['LIMIT'],
+               //              isset($options['OFFSET']) ? $options['OFFSET']
+               //              : false);
+               //}
+
+               if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $postLimitTail .= ' FOR UPDATE';
+               if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $postLimitTail .= ' LOCK IN SHARE MODE';
+               if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+
+               return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
+       }
+
+       public function setTimeout( $timeout ) {
+               // @todo fixme no-op
+       }
+
+       function ping() {
+               wfDebug( "Function ping() not written for DatabasePostgres.php yet");
+               return true;
+       }
+
+       /**
+        * How lagged is this slave?
+        *
+        */
+       public function getLag() {
+               # Not implemented for PostgreSQL
+               return false;
+       }
+
+       function setFakeSlaveLag() {}
+       function setFakeMaster() {}
+
+       function getDBname() {
+               return $this->mDBname;
+       }
+
+       function getServer() {
+               return $this->mServer;
+       }
+
+       function buildConcat( $stringList ) {
+               return implode( ' || ', $stringList );
+       }
+
+} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
new file mode 100644 (file)
index 0000000..8b4466e
--- /dev/null
@@ -0,0 +1,395 @@
+<?php
+/**
+ * This script is the SQLite database abstraction layer
+ *
+ * See maintenance/sqlite/README for development notes and other specific information
+ * @ingroup Database
+ * @file
+ */
+
+/**
+ * @ingroup Database
+ */
+class DatabaseSqlite extends Database {
+
+       var $mAffectedRows;
+       var $mLastResult;
+       var $mDatabaseFile;
+
+       /**
+        * Constructor
+        */
+       function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) {
+               global $wgOut,$wgSQLiteDataDir;
+               if ("$wgSQLiteDataDir" == '') $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data';
+               if (!is_dir($wgSQLiteDataDir)) mkdir($wgSQLiteDataDir,0700);
+               if (!isset($wgOut)) $wgOut = NULL; # Can't get a reference if it hasn't been set yet
+               $this->mOut =& $wgOut;
+               $this->mFailFunction = $failFunction;
+               $this->mFlags = $flags;
+               $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
+               $this->open($server, $user, $password, $dbName);
+       }
+
+       /**
+        * todo: check if these should be true like parent class
+        */
+       function implicitGroupby()   { return false; }
+       function implicitOrderby()   { return false; }
+
+       static function newFromParams($server, $user, $password, $dbName, $failFunction = false, $flags = 0) {
+               return new DatabaseSqlite($server, $user, $password, $dbName, $failFunction, $flags);
+       }
+
+       /** Open an SQLite database and return a resource handle to it
+        *  NOTE: only $dbName is used, the other parameters are irrelevant for SQLite databases
+        */
+       function open($server,$user,$pass,$dbName) {
+               $this->mConn = false;
+               if ($dbName) {
+                       $file = $this->mDatabaseFile;
+                       if ($this->mFlags & DBO_PERSISTENT) $this->mConn = new PDO("sqlite:$file",$user,$pass,array(PDO::ATTR_PERSISTENT => true));
+                       else $this->mConn = new PDO("sqlite:$file",$user,$pass);
+                       if ($this->mConn === false) wfDebug("DB connection error: $err\n");;
+                       $this->mOpened = $this->mConn;
+                       $this->mConn->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT); # set error codes only, dont raise exceptions
+               }
+               return $this->mConn;
+       }
+
+       /**
+        * Close an SQLite database
+        */
+       function close() {
+               $this->mOpened = false;
+               if (is_object($this->mConn)) {
+                       if ($this->trxLevel()) $this->immediateCommit();
+                       $this->mConn = null;
+               }
+               return true;
+       }
+
+       /**
+        * SQLite doesn't allow buffered results or data seeking etc, so we'll use fetchAll as the result
+        */
+       function doQuery($sql) {
+               $res = $this->mConn->query($sql);
+               if ($res === false) $this->reportQueryError($this->lastError(),$this->lastErrno(),$sql,__FUNCTION__);
+               else {
+                       $r = $res instanceof ResultWrapper ? $res->result : $res;
+                       $this->mAffectedRows = $r->rowCount();
+                       $res = new ResultWrapper($this,$r->fetchAll());
+               }
+               return $res;
+       }
+
+       function freeResult(&$res) {
+               if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
+       }
+
+       function fetchObject(&$res) {
+               if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+               $cur = current($r);
+               if (is_array($cur)) {
+                       next($r);
+                       $obj = new stdClass;
+                       foreach ($cur as $k => $v) if (!is_numeric($k)) $obj->$k = $v;
+                       return $obj;
+               }
+               return false;
+       }
+
+       function fetchRow(&$res) {
+               if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+               $cur = current($r);
+               if (is_array($cur)) {
+                       next($r);
+                       return $cur;
+               }
+               return false;
+       }
+
+       /**
+        * The PDO::Statement class implements the array interface so count() will work
+        */
+       function numRows(&$res) {
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+               return count($r);
+       }
+
+       function numFields(&$res) {
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+               return is_array($r) ? count($r[0]) : 0;
+       }
+
+       function fieldName(&$res,$n) {
+               $r = $res instanceof ResultWrapper ? $res->result : $res;
+               if (is_array($r)) {
+                       $keys = array_keys($r[0]);
+                       return $keys[$n];
+               }
+               return  false;
+       }
+
+       /**
+        * Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
+        */
+       function tableName($name) {
+               return str_replace('`','',parent::tableName($name));
+       }
+
+       /**
+        * This must be called after nextSequenceVal
+        */
+       function insertId() {
+               return $this->mConn->lastInsertId();
+       }
+
+       function dataSeek(&$res,$row) {
+               if ($res instanceof ResultWrapper) $r =& $res->result; else $r =& $res;
+               reset($r);
+               if ($row > 0) for ($i = 0; $i < $row; $i++) next($r);
+       }
+
+       function lastError() {
+               if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
+               $e = $this->mConn->errorInfo();
+               return isset($e[2]) ? $e[2] : '';
+       }
+
+       function lastErrno() {
+               if (!is_object($this->mConn)) return "Cannot return last error, no db connection";
+               return $this->mConn->errorCode();
+       }
+
+       function affectedRows() {
+               return $this->mAffectedRows;
+       }
+
+       /**
+        * Returns information about an index
+        * - if errors are explicitly ignored, returns NULL on failure
+        */
+       function indexInfo($table, $index, $fname = 'Database::indexExists') {
+               return false;
+       }
+
+       function indexUnique($table, $index, $fname = 'Database::indexUnique') {
+               return false;
+       }
+
+       /**
+        * Filter the options used in SELECT statements
+        */
+       function makeSelectOptions($options) {
+               foreach ($options as $k => $v) if (is_numeric($k) && $v == 'FOR UPDATE') $options[$k] = '';
+               return parent::makeSelectOptions($options);
+       }
+
+       /**
+        * Based on MySQL method (parent) with some prior SQLite-sepcific adjustments
+        */
+       function insert($table, $a, $fname = 'DatabaseSqlite::insert', $options = array()) {
+               if (!count($a)) return true;
+               if (!is_array($options)) $options = array($options);
+
+               # SQLite uses OR IGNORE not just IGNORE
+               foreach ($options as $k => $v) if ($v == 'IGNORE') $options[$k] = 'OR IGNORE';
+
+               # SQLite can't handle multi-row inserts, so divide up into multiple single-row inserts
+               if (isset($a[0]) && is_array($a[0])) {
+                       $ret = true;
+                       foreach ($a as $k => $v) if (!parent::insert($table,$v,"$fname/multi-row",$options)) $ret = false;
+               }
+               else $ret = parent::insert($table,$a,"$fname/single-row",$options);
+
+               return $ret;
+       }
+
+       /**
+        * SQLite does not have a "USE INDEX" clause, so return an empty string
+        */
+       function useIndexClause($index) {
+               return '';
+       }
+
+       # Returns the size of a text field, or -1 for "unlimited"
+       function textFieldSize($table, $field) {
+               return -1;
+       }
+
+       /**
+        * No low priority option in SQLite
+        */
+       function lowPriorityOption() {
+               return '';
+       }
+
+       /**
+        * Returns an SQL expression for a simple conditional.
+        * - uses CASE on SQLite
+        */
+       function conditional($cond, $trueVal, $falseVal) {
+               return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+       }
+
+       function wasDeadlock() {
+               return $this->lastErrno() == SQLITE_BUSY;
+       }
+
+       /**
+        * @return string wikitext of a link to the server software's web site
+        */
+       function getSoftwareLink() {
+               return "[http://sqlite.org/ SQLite]";
+       }
+
+       /**
+        * @return string Version information from the database
+        */
+       function getServerVersion() {
+               global $wgContLang;
+               $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
+               $size = $wgContLang->formatSize(filesize($this->mDatabaseFile));
+               $file = basename($this->mDatabaseFile);
+               return $ver." ($file: $size)";
+       }
+
+       /**
+        * Query whether a given column exists in the mediawiki schema
+        */
+       function fieldExists($table, $field) { return true; }
+
+       function fieldInfo($table, $field) { return SQLiteField::fromText($this, $table, $field); }
+
+       function begin() {
+               if ($this->mTrxLevel == 1) $this->commit();
+               $this->mConn->beginTransaction();
+               $this->mTrxLevel = 1;
+       }
+
+       function commit() {
+               if ($this->mTrxLevel == 0) return;
+               $this->mConn->commit();
+               $this->mTrxLevel = 0;
+       }
+
+       function rollback() {
+               if ($this->mTrxLevel == 0) return;
+               $this->mConn->rollBack();
+               $this->mTrxLevel = 0;
+       }
+
+       function limitResultForUpdate($sql, $num) {
+               return $sql;
+       }
+
+       function strencode($s) {
+               return substr($this->addQuotes($s),1,-1);
+       }
+
+       function encodeBlob($b) {
+               return $this->strencode($b);
+       }
+
+       function decodeBlob($b) {
+               return $b;
+       }
+
+       function addQuotes($s) {
+               return $this->mConn->quote($s);
+       }
+
+       function quote_ident($s) { return $s; }
+
+       /**
+        * For now, does nothing
+        */
+       function selectDB($db) { return true; }
+
+       /**
+        * not done
+        */
+       public function setTimeout($timeout) { return; }
+
+       function ping() {
+               wfDebug("Function ping() not written for SQLite yet");
+               return true;
+       }
+
+       /**
+        * How lagged is this slave?
+        */
+       public function getLag() {
+               return 0;
+       }
+
+       /**
+        * Called by the installer script (when modified according to the MediaWikiLite installation instructions)
+        * - this is the same way PostgreSQL works, MySQL reads in tables.sql and interwiki.sql using dbsource (which calls db->sourceFile)
+        */
+       public function setup_database() {
+               global $IP,$wgSQLiteDataDir,$wgDBTableOptions;
+               $wgDBTableOptions = '';
+               $mysql_tmpl  = "$IP/maintenance/tables.sql";
+               $mysql_iw    = "$IP/maintenance/interwiki.sql";
+               $sqlite_tmpl = "$IP/maintenance/sqlite/tables.sql";
+
+               # Make an SQLite template file if it doesn't exist (based on the same one MySQL uses to create a new wiki db)
+               if (!file_exists($sqlite_tmpl)) {
+                       $sql = file_get_contents($mysql_tmpl);
+                       $sql = preg_replace('/^\s*--.*?$/m','',$sql); # strip comments
+                       $sql = preg_replace('/^\s*(UNIQUE)?\s*(PRIMARY)?\s*KEY.+?$/m','',$sql);
+                       $sql = preg_replace('/^\s*(UNIQUE )?INDEX.+?$/m','',$sql); # These indexes should be created with a CREATE INDEX query
+                       $sql = preg_replace('/^\s*FULLTEXT.+?$/m','',$sql);        # Full text indexes
+                       $sql = preg_replace('/ENUM\(.+?\)/','TEXT',$sql); # Make ENUM's into TEXT's
+                       $sql = preg_replace('/binary\(\d+\)/','BLOB',$sql);
+                       $sql = preg_replace('/(TYPE|MAX_ROWS|AVG_ROW_LENGTH)=\w+/','',$sql);
+                       $sql = preg_replace('/,\s*\)/s',')',$sql); # removing previous items may leave a trailing comma
+                       $sql = str_replace('binary','',$sql);
+                       $sql = str_replace('auto_increment','PRIMARY KEY AUTOINCREMENT',$sql);
+                       $sql = str_replace(' unsigned','',$sql);
+                       $sql = str_replace(' int ',' INTEGER ',$sql);
+                       $sql = str_replace('NOT NULL','',$sql);
+
+                       # Tidy up and write file
+                       $sql = preg_replace('/^\s*^/m','',$sql); # Remove empty lines
+                       $sql = preg_replace('/;$/m',";\n",$sql); # Separate each statement with an empty line
+                       file_put_contents($sqlite_tmpl,$sql);
+               }
+
+               # Parse the SQLite template replacing inline variables such as /*$wgDBprefix*/
+               $err = $this->sourceFile($sqlite_tmpl);
+               if ($err !== true) $this->reportQueryError($err,0,$sql,__FUNCTION__);
+
+               # Use DatabasePostgres's code to populate interwiki from MySQL template
+               $f = fopen($mysql_iw,'r');
+               if ($f == false) dieout("<li>Could not find the interwiki.sql file");
+               $sql = "INSERT INTO interwiki(iw_prefix,iw_url,iw_local) VALUES ";
+               while (!feof($f)) {
+                       $line = fgets($f,1024);
+                       $matches = array();
+                       if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
+                       $this->query("$sql $matches[1],$matches[2])");
+               }
+       }
+
+}
+
+/**
+ * @ingroup Database
+ */
+class SQLiteField extends MySQLField {
+
+       function __construct() {
+       }
+
+       static function fromText($db, $table, $field) {
+               $n = new SQLiteField;
+               $n->name = $field;
+               $n->tablename = $table;
+               return $n;
+       }
+
+} // end DatabaseSqlite class
+
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
new file mode 100644 (file)
index 0000000..e7b2778
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+/**
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * An interface for generating database load balancers
+ * @ingroup Database
+ */
+abstract class LBFactory {
+       static $instance;
+
+       /**
+        * Get an LBFactory instance
+        */
+       static function &singleton() {
+               if ( is_null( self::$instance ) ) {
+                       global $wgLBFactoryConf;
+                       $class = $wgLBFactoryConf['class'];
+                       self::$instance = new $class( $wgLBFactoryConf );
+               }
+               return self::$instance;
+       }
+
+       /**
+        * Destory the instance
+        * Actually used by maintenace/parserTests.inc to force to reopen connection
+        * when $wgDBprefix has changed
+        */
+       static function destroy(){
+               self::$instance = null;
+       }
+
+       /**
+        * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
+        */
+       abstract function __construct( $conf );
+
+       /**
+        * Get a load balancer object.
+        *
+        * @param string $wiki Wiki ID, or false for the current wiki
+        * @return LoadBalancer
+        */
+       abstract function getMainLB( $wiki = false );
+
+       /*
+        * Get a load balancer for external storage
+        *
+        * @param string $cluster External storage cluster, or false for core
+        * @param string $wiki Wiki ID, or false for the current wiki
+        */
+       abstract function &getExternalLB( $cluster, $wiki = false );
+
+       /**
+        * Execute a function for each tracked load balancer
+        * The callback is called with the load balancer as the first parameter,
+        * and $params passed as the subsequent parameters.
+        */
+       abstract function forEachLB( $callback, $params = array() );
+
+       /**
+        * Prepare all load balancers for shutdown
+        * STUB
+        */
+       function shutdown() {}
+
+       /**
+        * Call a method of each load balancer
+        */
+       function forEachLBCallMethod( $methodName, $args = array() ) {
+               $this->forEachLB( array( $this, 'callMethod' ), array( $methodName, $args ) );
+       }
+
+       /**
+        * Private helper for forEachLBCallMethod
+        */
+       function callMethod( $loadBalancer, $methodName, $args ) {
+               call_user_func_array( array( $loadBalancer, $methodName ), $args );
+       }
+
+       /**
+        * Commit changes on all master connections
+        */
+       function commitMasterChanges() {
+               $this->forEachLBCallMethod( 'commitMasterChanges' );
+       }
+}
+
+/**
+ * A simple single-master LBFactory that gets its configuration from the b/c globals
+ */
+class LBFactory_Simple extends LBFactory {
+       var $mainLB;
+       var $extLBs = array();
+
+       # Chronology protector
+       var $chronProt;
+
+       function __construct( $conf ) {
+               $this->chronProt = new ChronologyProtector;
+       }
+
+       function getMainLB( $wiki = false ) {
+               if ( !isset( $this->mainLB ) ) {
+                       global $wgDBservers, $wgMasterWaitTimeout;
+                       if ( !$wgDBservers ) {
+                               global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
+                               $wgDBservers = array(array(
+                                       'host' => $wgDBserver,
+                                       'user' => $wgDBuser,
+                                       'password' => $wgDBpassword,
+                                       'dbname' => $wgDBname,
+                                       'type' => $wgDBtype,
+                                       'load' => 1,
+                                       'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
+                               ));
+                       }
+
+                       $this->mainLB = new LoadBalancer( $wgDBservers, false, $wgMasterWaitTimeout, true );
+                       $this->mainLB->parentInfo( array( 'id' => 'main' ) );
+                       $this->chronProt->initLB( $this->mainLB );
+               }
+               return $this->mainLB;
+       }
+
+       function &getExternalLB( $cluster, $wiki = false ) {
+               global $wgExternalServers;
+               if ( !isset( $this->extLBs[$cluster] ) ) {
+                       if ( !isset( $wgExternalServers[$cluster] ) ) {
+                               throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+                       }
+                       $this->extLBs[$cluster] = new LoadBalancer( $wgExternalServers[$cluster] );
+                       $this->extLBs[$cluster]->parentInfo( array( 'id' => "ext-$cluster" ) );
+               }
+               return $this->extLBs[$cluster];
+       }
+
+       /**
+        * Execute a function for each tracked load balancer
+        * The callback is called with the load balancer as the first parameter,
+        * and $params passed as the subsequent parameters.
+        */
+       function forEachLB( $callback, $params = array() ) {
+               if ( isset( $this->mainLB ) ) {
+                       call_user_func_array( $callback, array_merge( array( $this->mainLB ), $params ) );
+               }
+               foreach ( $this->extLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+               }
+       }
+
+       function shutdown() {
+               if ( $this->mainLB ) {
+                       $this->chronProt->shutdownLB( $this->mainLB );
+               }
+               $this->chronProt->shutdown();
+               $this->commitMasterChanges();
+       }
+}
+
+/**
+ * Class for ensuring a consistent ordering of events as seen by the user, despite replication.
+ * Kind of like Hawking's [[Chronology Protection Agency]].
+ */
+class ChronologyProtector {
+       var $startupPos;
+       var $shutdownPos = array();
+
+       /**
+        * Initialise a LoadBalancer to give it appropriate chronology protection.
+        *
+        * @param LoadBalancer $lb
+        */
+       function initLB( $lb ) {
+               if ( $this->startupPos === null ) {
+                       if ( !empty( $_SESSION[__CLASS__] ) ) {
+                               $this->startupPos = $_SESSION[__CLASS__];
+                       }
+               }
+               if ( !$this->startupPos ) {
+                       return;
+               }
+               $masterName = $lb->getServerName( 0 );
+
+               if ( $lb->getServerCount() > 1 && !empty( $this->startupPos[$masterName] ) ) {
+                       $info = $lb->parentInfo();
+                       $pos = $this->startupPos[$masterName];
+                       wfDebug( __METHOD__.": LB " . $info['id'] . " waiting for master pos $pos\n" );
+                       $lb->waitFor( $this->startupPos[$masterName] );
+               }
+       }
+
+       /**
+        * Notify the ChronologyProtector that the LoadBalancer is about to shut
+        * down. Saves replication positions.
+        *
+        * @param LoadBalancer $lb
+        */
+       function shutdownLB( $lb ) {
+               if ( session_id() != '' && $lb->getServerCount() > 1 ) {
+                       $masterName = $lb->getServerName( 0 );
+                       if ( !isset( $this->shutdownPos[$masterName] ) ) {
+                               $pos = $lb->getMasterPos();
+                               $info = $lb->parentInfo();
+                               wfDebug( __METHOD__.": LB " . $info['id'] . " has master pos $pos\n" );
+                               $this->shutdownPos[$masterName] = $pos;
+                       }
+               }
+       }
+
+       /**
+        * Notify the ChronologyProtector that the LBFactory is done calling shutdownLB() for now.
+        * May commit chronology data to persistent storage.
+        */
+       function shutdown() {
+               if ( session_id() != '' && count( $this->shutdownPos ) ) {
+                       wfDebug( __METHOD__.": saving master pos for " .
+                               count( $this->shutdownPos ) . " master(s)\n" );
+                       $_SESSION[__CLASS__] = $this->shutdownPos;
+               }
+       }
+}
diff --git a/includes/db/LBFactory_Multi.php b/includes/db/LBFactory_Multi.php
new file mode 100644 (file)
index 0000000..b5fc1f6
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+/**
+ * @file
+ * @ingroup Database
+ */
+
+
+/**
+ * A multi-wiki, multi-master factory for Wikimedia and similar installations.
+ * Ignores the old configuration globals
+ *
+ * Configuration:
+ *     sectionsByDB                A map of database names to section names
+ *
+ *     sectionLoads                A 2-d map. For each section, gives a map of server names to load ratios.
+ *                                 For example: array( 'section1' => array( 'db1' => 100, 'db2' => 100 ) )
+ *
+ *     serverTemplate              A server info associative array as documented for $wgDBservers. The host,
+ *                                 hostName and load entries will be overridden.
+ *
+ *     groupLoadsBySection         A 3-d map giving server load ratios for each section and group. For example:
+ *                                 array( 'section1' => array( 'group1' => array( 'db1' => 100, 'db2' => 100 ) ) )
+ *
+ *     groupLoadsByDB              A 3-d map giving server load ratios by DB name.
+ *
+ *     hostsByName                 A map of hostname to IP address.
+ *
+ *     externalLoads               A map of external storage cluster name to server load map
+ *
+ *     externalTemplateOverrides   A set of server info keys overriding serverTemplate for external storage
+ *
+ *     templateOverridesByServer   A 2-d map overriding serverTemplate and externalTemplateOverrides on a
+ *                                 server-by-server basis. Applies to both core and external storage.
+ *
+ *     templateOverridesByCluster  A 2-d map overriding the server info by external storage cluster
+ *
+ *     masterTemplateOverrides     An override array for all master servers.
+ *
+ * @ingroup Database
+ */
+class LBFactory_Multi extends LBFactory {
+       // Required settings
+       var $sectionsByDB, $sectionLoads, $serverTemplate;
+       // Optional settings
+       var $groupLoadsBySection = array(), $groupLoadsByDB = array(), $hostsByName = array();
+       var $externalLoads = array(), $externalTemplateOverrides, $templateOverridesByServer;
+       var $templateOverridesByCluster, $masterTemplateOverrides;
+       // Other stuff
+       var $conf, $mainLBs = array(), $extLBs = array();
+       var $localSection = null;
+
+       function __construct( $conf ) {
+               $this->chronProt = new ChronologyProtector;
+               $this->conf = $conf;
+               $required = array( 'sectionsByDB', 'sectionLoads', 'serverTemplate' );
+               $optional = array( 'groupLoadsBySection', 'groupLoadsByDB', 'hostsByName',
+                       'externalLoads', 'externalTemplateOverrides', 'templateOverridesByServer',
+                       'templateOverridesByCluster', 'masterTemplateOverrides' );
+
+               foreach ( $required as $key ) {
+                       if ( !isset( $conf[$key] ) ) {
+                               throw new MWException( __CLASS__.": $key is required in configuration" );
+                       }
+                       $this->$key = $conf[$key];
+               }
+
+               foreach ( $optional as $key ) {
+                       if ( isset( $conf[$key] ) ) {
+                               $this->$key = $conf[$key];
+                       }
+               }
+       }
+
+       function getSectionForWiki( $wiki ) {
+               list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+               if ( isset( $this->sectionsByDB[$dbName] ) ) {
+                       return $this->sectionsByDB[$dbName];
+               } else {
+                       return 'DEFAULT';
+               }
+       }
+
+       function getMainLB( $wiki = false ) {
+               // Determine section
+               if ( $wiki === false ) {
+                       if ( $this->localSection === null ) {
+                               $this->localSection = $this->getSectionForWiki( $wiki );
+                       }
+                       $section = $this->localSection;
+               } else {
+                       $section = $this->getSectionForWiki( $wiki );
+               }
+
+               if ( !isset( $this->mainLBs[$section] ) ) {
+                       list( $dbName, $prefix ) = $this->getDBNameAndPrefix( $wiki );
+                       $groupLoads = array();
+                       if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
+                               $groupLoads = $this->groupLoadsByDB[$dbName];
+                       }
+                       if ( isset( $this->groupLoadsBySection[$section] ) ) {
+                               $groupLoads = array_merge_recursive( $groupLoads, $this->groupLoadsBySection[$section] );
+                       }
+                       $this->mainLBs[$section] = $this->newLoadBalancer( $this->serverTemplate,
+                               $this->sectionLoads[$section], $groupLoads, "main-$section" );
+                       $this->chronProt->initLB( $this->mainLBs[$section] );
+               }
+               return $this->mainLBs[$section];
+       }
+
+       function &getExternalLB( $cluster, $wiki = false ) {
+               if ( !isset( $this->extLBs[$cluster] ) ) {
+                       if ( !isset( $this->externalLoads[$cluster] ) ) {
+                               throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
+                       }
+                       $template = $this->serverTemplate;
+                       if ( isset( $this->externalTemplateOverrides ) ) {
+                               $template = $this->externalTemplateOverrides + $template;
+                       }
+                       if ( isset( $this->templateOverridesByCluster[$cluster] ) ) {
+                               $template = $this->templateOverridesByCluster[$cluster] + $template;
+                       }
+                       $this->extLBs[$cluster] = $this->newLoadBalancer( $template,
+                               $this->externalLoads[$cluster], array(), "ext-$cluster" );
+               }
+               return $this->extLBs[$cluster];
+       }
+
+       /**
+        * Make a new load balancer object based on template and load array
+        */
+       function newLoadBalancer( $template, $loads, $groupLoads, $id ) {
+               global $wgMasterWaitTimeout;
+               $servers = $this->makeServerArray( $template, $loads, $groupLoads );
+               $lb = new LoadBalancer( $servers, false, $wgMasterWaitTimeout );
+               $lb->parentInfo( array( 'id' => $id ) );
+               return $lb;
+       }
+
+       /**
+        * Make a server array as expected by LoadBalancer::__construct, using a template and load array
+        */
+       function makeServerArray( $template, $loads, $groupLoads ) {
+               $servers = array();
+               $master = true;
+               $groupLoadsByServer = $this->reindexGroupLoads( $groupLoads );
+               foreach ( $groupLoadsByServer as $server => $stuff ) {
+                       if ( !isset( $loads[$server] ) ) {
+                               $loads[$server] = 0;
+                       }
+               }
+               foreach ( $loads as $serverName => $load ) {
+                       $serverInfo = $template;
+                       if ( $master ) {
+                               $serverInfo['master'] = true;
+                               if ( isset( $this->masterTemplateOverrides ) ) {
+                                       $serverInfo = $this->masterTemplateOverrides + $serverInfo;
+                               }
+                               $master = false;
+                       }
+                       if ( isset( $this->templateOverridesByServer[$serverName] ) ) {
+                               $serverInfo = $this->templateOverridesByServer[$serverName] + $serverInfo;
+                       }
+                       if ( isset( $groupLoadsByServer[$serverName] ) ) {
+                               $serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
+                       }
+                       if ( isset( $this->hostsByName[$serverName] ) ) {
+                               $serverInfo['host'] = $this->hostsByName[$serverName];
+                       } else {
+                               $serverInfo['host'] = $serverName;
+                       }
+                       $serverInfo['hostName'] = $serverName;
+                       $serverInfo['load'] = $load;
+                       $servers[] = $serverInfo;
+               }
+               return $servers;
+       }
+
+       /**
+        * Take a group load array indexed by group then server, and reindex it by server then group
+        */
+       function reindexGroupLoads( $groupLoads ) {
+               $reindexed = array();
+               foreach ( $groupLoads as $group => $loads ) {
+                       foreach ( $loads as $server => $load ) {
+                               $reindexed[$server][$group] = $load;
+                       }
+               }
+               return $reindexed;
+       }
+
+       /**
+        * Get the database name and prefix based on the wiki ID
+        */
+       function getDBNameAndPrefix( $wiki = false ) {
+               if ( $wiki === false ) {
+                       global $wgDBname, $wgDBprefix;
+                       return array( $wgDBname, $wgDBprefix );
+               } else {
+                       return wfSplitWikiID( $wiki );
+               }
+       }
+
+       /**
+        * Execute a function for each tracked load balancer
+        * The callback is called with the load balancer as the first parameter,
+        * and $params passed as the subsequent parameters.
+        */
+       function forEachLB( $callback, $params = array() ) {
+               foreach ( $this->mainLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+               }
+               foreach ( $this->extLBs as $lb ) {
+                       call_user_func_array( $callback, array_merge( array( $lb ), $params ) );
+               }
+       }
+
+       function shutdown() {
+               foreach ( $this->mainLBs as $lb ) {
+                       $this->chronProt->shutdownLB( $lb );
+               }
+               $this->chronProt->shutdown();
+               $this->commitMasterChanges();
+       }
+}
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
new file mode 100644 (file)
index 0000000..4280713
--- /dev/null
@@ -0,0 +1,894 @@
+<?php
+/**
+ * @file
+ * @ingroup Database
+ */
+
+/**
+ * Database load balancing object
+ *
+ * @todo document
+ * @ingroup Database
+ */
+class LoadBalancer {
+       /* private */ var $mServers, $mConns, $mLoads, $mGroupLoads;
+       /* private */ var $mFailFunction, $mErrorConnection;
+       /* private */ var $mReadIndex, $mLastIndex, $mAllowLagged;
+       /* private */ var $mWaitForPos, $mWaitTimeout;
+       /* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
+       /* private */ var $mParentInfo, $mLagTimes;
+
+       function __construct( $servers, $failFunction = false, $waitTimeout = 10, $unused = false )
+       {
+               $this->mServers = $servers;
+               $this->mFailFunction = $failFunction;
+               $this->mReadIndex = -1;
+               $this->mWriteIndex = -1;
+               $this->mConns = array(
+                       'local' => array(),
+                       'foreignUsed' => array(),
+                       'foreignFree' => array() );
+               $this->mLastIndex = -1;
+               $this->mLoads = array();
+               $this->mWaitForPos = false;
+               $this->mWaitTimeout = $waitTimeout;
+               $this->mLaggedSlaveMode = false;
+               $this->mErrorConnection = false;
+               $this->mAllowLag = false;
+
+               foreach( $servers as $i => $server ) {
+                       $this->mLoads[$i] = $server['load'];
+                       if ( isset( $server['groupLoads'] ) ) {
+                               foreach ( $server['groupLoads'] as $group => $ratio ) {
+                                       if ( !isset( $this->mGroupLoads[$group] ) ) {
+                                               $this->mGroupLoads[$group] = array();
+                                       }
+                                       $this->mGroupLoads[$group][$i] = $ratio;
+                               }
+                       }
+               }
+       }
+
+       static function newFromParams( $servers, $failFunction = false, $waitTimeout = 10 )
+       {
+               return new LoadBalancer( $servers, $failFunction, $waitTimeout );
+       }
+
+       /**
+        * Get or set arbitrary data used by the parent object, usually an LBFactory
+        */
+       function parentInfo( $x = null ) {
+               return wfSetVar( $this->mParentInfo, $x );
+       }
+
+       /**
+        * Given an array of non-normalised probabilities, this function will select
+        * an element and return the appropriate key
+        */
+       function pickRandom( $weights )
+       {
+               if ( !is_array( $weights ) || count( $weights ) == 0 ) {
+                       return false;
+               }
+
+               $sum = array_sum( $weights );
+               if ( $sum == 0 ) {
+                       # No loads on any of them
+                       # In previous versions, this triggered an unweighted random selection,
+                       # but this feature has been removed as of April 2006 to allow for strict
+                       # separation of query groups.
+                       return false;
+               }
+               $max = mt_getrandmax();
+               $rand = mt_rand(0, $max) / $max * $sum;
+
+               $sum = 0;
+               foreach ( $weights as $i => $w ) {
+                       $sum += $w;
+                       if ( $sum >= $rand ) {
+                               break;
+                       }
+               }
+               return $i;
+       }
+
+       function getRandomNonLagged( $loads, $wiki = false ) {
+               # Unset excessively lagged servers
+               $lags = $this->getLagTimes( $wiki );
+               foreach ( $lags as $i => $lag ) {
+                       if ( $i != 0 && isset( $this->mServers[$i]['max lag'] ) ) {
+                               if ( $lag === false ) {
+                                       wfDebug( "Server #$i is not replicating\n" );
+                                       unset( $loads[$i] );
+                               } elseif ( $lag > $this->mServers[$i]['max lag'] ) {
+                                       wfDebug( "Server #$i is excessively lagged ($lag seconds)\n" );
+                                       unset( $loads[$i] );
+                               }
+                       }
+               }
+
+               # Find out if all the slaves with non-zero load are lagged
+               $sum = 0;
+               foreach ( $loads as $load ) {
+                       $sum += $load;
+               }
+               if ( $sum == 0 ) {
+                       # No appropriate DB servers except maybe the master and some slaves with zero load
+                       # Do NOT use the master
+                       # Instead, this function will return false, triggering read-only mode,
+                       # and a lagged slave will be used instead.
+                       return false;
+               }
+
+               if ( count( $loads ) == 0 ) {
+                       return false;
+               }
+
+               #wfDebugLog( 'connect', var_export( $loads, true ) );
+
+               # Return a random representative of the remainder
+               return $this->pickRandom( $loads );
+       }
+
+       /**
+        * Get the index of the reader connection, which may be a slave
+        * This takes into account load ratios and lag times. It should
+        * always return a consistent index during a given invocation
+        *
+        * Side effect: opens connections to databases
+        */
+       function getReaderIndex( $group = false, $wiki = false ) {
+               global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll, $wgDBtype;
+
+               # FIXME: For now, only go through all this for mysql databases
+               if ($wgDBtype != 'mysql') {
+                       return $this->getWriterIndex();
+               }
+
+               if ( count( $this->mServers ) == 1 )  {
+                       # Skip the load balancing if there's only one server
+                       return 0;
+               } elseif ( $group === false and $this->mReadIndex >= 0 ) {
+                       # Shortcut if generic reader exists already
+                       return $this->mReadIndex;
+               }
+
+               wfProfileIn( __METHOD__ );
+
+               $totalElapsed = 0;
+
+               # convert from seconds to microseconds
+               $timeout = $wgDBClusterTimeout * 1e6;
+
+               # Find the relevant load array
+               if ( $group !== false ) {
+                       if ( isset( $this->mGroupLoads[$group] ) ) {
+                               $nonErrorLoads = $this->mGroupLoads[$group];
+                       } else {
+                               # No loads for this group, return false and the caller can use some other group
+                               wfDebug( __METHOD__.": no loads for group $group\n" );
+                               wfProfileOut( __METHOD__ );
+                               return false;
+                       }
+               } else {
+                       $nonErrorLoads = $this->mLoads;
+               }
+
+               if ( !$nonErrorLoads ) {
+                       throw new MWException( "Empty server array given to LoadBalancer" );
+               }
+
+               $i = false;
+               $found = false;
+               $laggedSlaveMode = false;
+
+               # First try quickly looking through the available servers for a server that
+               # meets our criteria
+               do {
+                       $totalThreadsConnected = 0;
+                       $overloadedServers = 0;
+                       $currentLoads = $nonErrorLoads;
+                       while ( count( $currentLoads ) ) {
+                               if ( $wgReadOnly || $this->mAllowLagged || $laggedSlaveMode ) {
+                                       $i = $this->pickRandom( $currentLoads );
+                               } else {
+                                       $i = $this->getRandomNonLagged( $currentLoads, $wiki );
+                                       if ( $i === false && count( $currentLoads ) != 0 )  {
+                                               # All slaves lagged. Switch to read-only mode
+                                               $wgReadOnly = wfMsgNoDBForContent( 'readonly_lag' );
+                                               $i = $this->pickRandom( $currentLoads );
+                                               $laggedSlaveMode = true;
+                                       }
+                               }
+
+                               if ( $i === false ) {
+                                       # pickRandom() returned false
+                                       # This is permanent and means the configuration wants us to return false
+                                       wfDebugLog( 'connect', __METHOD__.": pickRandom() returned false\n" );
+                                       wfProfileOut( __METHOD__ );
+                                       return false;
+                               }
+
+                               wfDebugLog( 'connect', __METHOD__.": Using reader #$i: {$this->mServers[$i]['host']}...\n" );
+                               $conn = $this->openConnection( $i, $wiki );
+
+                               if ( !$conn ) {
+                                       wfDebugLog( 'connect', __METHOD__.": Failed connecting to $i/$wiki\n" );
+                                       unset( $nonErrorLoads[$i] );
+                                       unset( $currentLoads[$i] );
+                                       continue;
+                               }
+
+                               if ( isset( $this->mServers[$i]['max threads'] ) ) {
+                                       $status = $conn->getStatus("Thread%");
+                                       if ( $wiki !== false ) {
+                                               $this->reuseConnection( $conn );
+                                       }
+                                       if ( $status['Threads_running'] > $this->mServers[$i]['max threads'] ) {
+                                               $totalThreadsConnected += $status['Threads_connected'];
+                                               $overloadedServers++;
+                                               unset( $currentLoads[$i] );
+                                       } else {
+                                               # Max threads satisfied, return this server
+                                               break 2;
+                                       }
+                               } else {
+                                       # No maximum, return this server
+                                       if ( $wiki !== false ) {
+                                               $this->reuseConnection( $conn );
+                                       }
+                                       $found = true;
+                                       break 2;
+                               }
+                       }
+
+                       # No server found yet
+                       $i = false;
+
+                       # If all servers were down, quit now
+                       if ( !count( $nonErrorLoads ) ) {
+                               wfDebugLog( 'connect', "All servers down\n" );
+                               break;
+                       }
+
+                       # Some servers must have been overloaded
+                       if ( $overloadedServers == 0 ) {
+                               throw new MWException( __METHOD__.": unexpectedly found no overloaded servers" );
+                       }
+                       # Back off for a while
+                       # Scale the sleep time by the number of connected threads, to produce a
+                       # roughly constant global poll rate
+                       $avgThreads = $totalThreadsConnected / $overloadedServers;
+                       $totalElapsed += $this->sleep( $wgDBAvgStatusPoll * $avgThreads );
+               } while ( $totalElapsed < $timeout );
+
+               if ( $totalElapsed >= $timeout ) {
+                       wfDebugLog( 'connect', "All servers busy\n" );
+                       $this->mErrorConnection = false;
+                       $this->mLastError = 'All servers busy';
+               }
+
+               if ( $i !== false ) {
+                       # Wait for the session master pos for a short time
+                       if ( $this->mWaitForPos && $i > 0 ) {
+                               if ( !$this->doWait( $i ) ) {
+                                       $this->mServers[$i]['slave pos'] = $conn->getSlavePos();
+                               }
+                       }
+                       if ( $this->mReadIndex <=0 && $this->mLoads[$i]>0 && $i !== false ) {
+                               $this->mReadIndex = $i;
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               return $i;
+       }
+
+       /**
+        * Wait for a specified number of microseconds, and return the period waited
+        */
+       function sleep( $t ) {
+               wfProfileIn( __METHOD__ );
+               wfDebug( __METHOD__.": waiting $t us\n" );
+               usleep( $t );
+               wfProfileOut( __METHOD__ );
+               return $t;
+       }
+
+       /**
+        * Get a random server to use in a query group
+        * @deprecated use getReaderIndex
+        */
+       function getGroupIndex( $group ) {
+               return $this->getReaderIndex( $group );
+       }
+
+       /**
+        * Set the master wait position
+        * If a DB_SLAVE connection has been opened already, waits
+        * Otherwise sets a variable telling it to wait if such a connection is opened
+        */
+       public function waitFor( $pos ) {
+               wfProfileIn( __METHOD__ );
+               $this->mWaitForPos = $pos;
+               $i = $this->mReadIndex;
+
+               if ( $i > 0 ) {
+                       if ( !$this->doWait( $i ) ) {
+                               $this->mServers[$i]['slave pos'] = $this->getAnyOpenConnection( $i )->getSlavePos();
+                               $this->mLaggedSlaveMode = true;
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Get any open connection to a given server index, local or foreign
+        * Returns false if there is no connection open
+        */
+       function getAnyOpenConnection( $i ) {
+               foreach ( $this->mConns as $type => $conns ) {
+                       if ( !empty( $conns[$i] ) ) {
+                               return reset( $conns[$i] );
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Wait for a given slave to catch up to the master pos stored in $this
+        */
+       function doWait( $index ) {
+               # Find a connection to wait on
+               $conn = $this->getAnyOpenConnection( $index );
+               if ( !$conn ) {
+                       wfDebug( __METHOD__ . ": no connection open\n" );
+                       return false;
+               }
+
+               wfDebug( __METHOD__.": Waiting for slave #$index to catch up...\n" );
+               $result = $conn->masterPosWait( $this->mWaitForPos, $this->mWaitTimeout );
+
+               if ( $result == -1 || is_null( $result ) ) {
+                       # Timed out waiting for slave, use master instead
+                       wfDebug( __METHOD__.": Timed out waiting for slave #$index pos {$this->mWaitForPos}\n" );
+                       return false;
+               } else {
+                       wfDebug( __METHOD__.": Done\n" );
+                       return true;
+               }
+       }
+
+       /**
+        * Get a connection by index
+        * This is the main entry point for this class.
+        */
+       public function &getConnection( $i, $groups = array(), $wiki = false ) {
+               global $wgDBtype;
+               wfProfileIn( __METHOD__ );
+
+               if ( $wiki === wfWikiID() ) {
+                       $wiki = false;
+               }
+
+               # Query groups
+               if ( $i == DB_MASTER ) {
+                       $i = $this->getWriterIndex();
+               } elseif ( !is_array( $groups ) ) {
+                       $groupIndex = $this->getReaderIndex( $groups, $wiki );
+                       if ( $groupIndex !== false ) {
+                               $serverName = $this->getServerName( $groupIndex );
+                               wfDebug( __METHOD__.": using server $serverName for group $groups\n" );
+                               $i = $groupIndex;
+                       }
+               } else {
+                       foreach ( $groups as $group ) {
+                               $groupIndex = $this->getReaderIndex( $group, $wiki );
+                               if ( $groupIndex !== false ) {
+                                       $serverName = $this->getServerName( $groupIndex );
+                                       wfDebug( __METHOD__.": using server $serverName for group $group\n" );
+                                       $i = $groupIndex;
+                                       break;
+                               }
+                       }
+               }
+
+               # Operation-based index
+               if ( $i == DB_SLAVE ) {
+                       $i = $this->getReaderIndex( false, $wiki );
+               } elseif ( $i == DB_LAST ) {
+                       # Just use $this->mLastIndex, which should already be set
+                       $i = $this->mLastIndex;
+                       if ( $i === -1 ) {
+                               # Oh dear, not set, best to use the writer for safety
+                               wfDebug( "Warning: DB_LAST used when there was no previous index\n" );
+                               $i = $this->getWriterIndex();
+                       }
+               }
+               # Couldn't find a working server in getReaderIndex()?
+               if ( $i === false ) {
+                       $this->reportConnectionError( $this->mErrorConnection );
+               }
+
+               # Now we have an explicit index into the servers array
+               $conn = $this->openConnection( $i, $wiki );
+               if ( !$conn ) {
+                       $this->reportConnectionError( $this->mErrorConnection );
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $conn;
+       }
+
+       /**
+        * Mark a foreign connection as being available for reuse under a different
+        * DB name or prefix. This mechanism is reference-counted, and must be called
+        * the same number of times as getConnection() to work.
+        */
+       public function reuseConnection( $conn ) {
+               $serverIndex = $conn->getLBInfo('serverIndex');
+               $refCount = $conn->getLBInfo('foreignPoolRefCount');
+               $dbName = $conn->getDBname();
+               $prefix = $conn->tablePrefix();
+               if ( strval( $prefix ) !== '' ) {
+                       $wiki = "$dbName-$prefix";
+               } else {
+                       $wiki = $dbName;
+               }
+               if ( $serverIndex === null || $refCount === null ) {
+                       wfDebug( __METHOD__.": this connection was not opened as a foreign connection\n" );
+                       /**
+                        * This can happen in code like:
+                        *   foreach ( $dbs as $db ) {
+                        *     $conn = $lb->getConnection( DB_SLAVE, array(), $db );
+                        *     ...
+                        *     $lb->reuseConnection( $conn );
+                        *   }
+                        * When a connection to the local DB is opened in this way, reuseConnection()
+                        * should be ignored
+                        */
+                       return;
+               }
+               if ( $this->mConns['foreignUsed'][$serverIndex][$wiki] !== $conn ) {
+                       throw new MWException( __METHOD__.": connection not found, has the connection been freed already?" );
+               }
+               $conn->setLBInfo( 'foreignPoolRefCount', --$refCount );
+               if ( $refCount <= 0 ) {
+                       $this->mConns['foreignFree'][$serverIndex][$wiki] = $conn;
+                       unset( $this->mConns['foreignUsed'][$serverIndex][$wiki] );
+                       wfDebug( __METHOD__.": freed connection $serverIndex/$wiki\n" );
+               } else {
+                       wfDebug( __METHOD__.": reference count for $serverIndex/$wiki reduced to $refCount\n" );
+               }
+       }
+
+       /**
+        * Open a connection to the server given by the specified index
+        * Index must be an actual index into the array.
+        * If the server is already open, returns it.
+        *
+        * On error, returns false, and the connection which caused the
+        * error will be available via $this->mErrorConnection.
+        *
+        * @param integer $i Server index
+        * @param string $wiki Wiki ID to open
+        * @return Database
+        *
+        * @access private
+        */
+       function openConnection( $i, $wiki = false ) {
+               wfProfileIn( __METHOD__ );
+               if ( $wiki !== false ) {
+                       $conn = $this->openForeignConnection( $i, $wiki );
+                       wfProfileOut( __METHOD__);
+                       return $conn;
+               }
+               if ( isset( $this->mConns['local'][$i][0] ) ) {
+                       $conn = $this->mConns['local'][$i][0];
+               } else {
+                       $server = $this->mServers[$i];
+                       $server['serverIndex'] = $i;
+                       $conn = $this->reallyOpenConnection( $server );
+                       if ( $conn->isOpen() ) {
+                               $this->mConns['local'][$i][0] = $conn;
+                       } else {
+                               wfDebug( "Failed to connect to database $i at {$this->mServers[$i]['host']}\n" );
+                               $this->mErrorConnection = $conn;
+                               $conn = false;
+                       }
+               }
+               $this->mLastIndex = $i;
+               wfProfileOut( __METHOD__ );
+               return $conn;
+       }
+
+       /**
+        * Open a connection to a foreign DB, or return one if it is already open.
+        *
+        * Increments a reference count on the returned connection which locks the
+        * connection to the requested wiki. This reference count can be
+        * decremented by calling reuseConnection().
+        *
+        * If a connection is open to the appropriate server already, but with the wrong
+        * database, it will be switched to the right database and returned, as long as
+        * it has been freed first with reuseConnection().
+        *
+        * On error, returns false, and the connection which caused the
+        * error will be available via $this->mErrorConnection.
+        *
+        * @param integer $i Server index
+        * @param string $wiki Wiki ID to open
+        * @return Database
+        */
+       function openForeignConnection( $i, $wiki ) {
+               wfProfileIn(__METHOD__);
+               list( $dbName, $prefix ) = wfSplitWikiID( $wiki );
+               if ( isset( $this->mConns['foreignUsed'][$i][$wiki] ) ) {
+                       // Reuse an already-used connection
+                       $conn = $this->mConns['foreignUsed'][$i][$wiki];
+                       wfDebug( __METHOD__.": reusing connection $i/$wiki\n" );
+               } elseif ( isset( $this->mConns['foreignFree'][$i][$wiki] ) ) {
+                       // Reuse a free connection for the same wiki
+                       $conn = $this->mConns['foreignFree'][$i][$wiki];
+                       unset( $this->mConns['foreignFree'][$i][$wiki] );
+                       $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+                       wfDebug( __METHOD__.": reusing free connection $i/$wiki\n" );
+               } elseif ( !empty( $this->mConns['foreignFree'][$i] ) ) {
+                       // Reuse a connection from another wiki
+                       $conn = reset( $this->mConns['foreignFree'][$i] );
+                       $oldWiki = key( $this->mConns['foreignFree'][$i] );
+
+                       if ( !$conn->selectDB( $dbName ) ) {
+                               global $wguname;
+                               $this->mLastError = "Error selecting database $dbName on server " .
+                                       $conn->getServer() . " from client host {$wguname['nodename']}\n";
+                               $this->mErrorConnection = $conn;
+                               $conn = false;
+                       } else {
+                               $conn->tablePrefix( $prefix );
+                               unset( $this->mConns['foreignFree'][$i][$oldWiki] );
+                               $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+                               wfDebug( __METHOD__.": reusing free connection from $oldWiki for $wiki\n" );
+                       }
+               } else {
+                       // Open a new connection
+                       $server = $this->mServers[$i];
+                       $server['serverIndex'] = $i;
+                       $server['foreignPoolRefCount'] = 0;
+                       $conn = $this->reallyOpenConnection( $server, $dbName );
+                       if ( !$conn->isOpen() ) {
+                               wfDebug( __METHOD__.": error opening connection for $i/$wiki\n" );
+                               $this->mErrorConnection = $conn;
+                               $conn = false;
+                       } else {
+                               $this->mConns['foreignUsed'][$i][$wiki] = $conn;
+                               wfDebug( __METHOD__.": opened new connection for $i/$wiki\n" );
+                       }
+               }
+
+               // Increment reference count
+               if ( $conn ) {
+                       $refCount = $conn->getLBInfo( 'foreignPoolRefCount' );
+                       $conn->setLBInfo( 'foreignPoolRefCount', $refCount + 1 );
+               }
+               wfProfileOut(__METHOD__);
+               return $conn;
+       }
+
+       /**
+        * Test if the specified index represents an open connection
+        * @access private
+        */
+       function isOpen( $index ) {
+               if( !is_integer( $index ) ) {
+                       return false;
+               }
+               return (bool)$this->getAnyOpenConnection( $index );
+       }
+
+       /**
+        * Really opens a connection. Uncached.
+        * Returns a Database object whether or not the connection was successful.
+        * @access private
+        */
+       function reallyOpenConnection( $server, $dbNameOverride = false ) {
+               if( !is_array( $server ) ) {
+                       throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
+               }
+
+               extract( $server );
+               if ( $dbNameOverride !== false ) {
+                       $dbname = $dbNameOverride;
+               }
+
+               # Get class for this database type
+               $class = 'Database' . ucfirst( $type );
+
+               # Create object
+               wfDebug( "Connecting to $host $dbname...\n" );
+               $db = new $class( $host, $user, $password, $dbname, 1, $flags );
+               if ( $db->isOpen() ) {
+                       wfDebug( "Connected\n" );
+               } else {
+                       wfDebug( "Failed\n" );
+               }
+               $db->setLBInfo( $server );
+               if ( isset( $server['fakeSlaveLag'] ) ) {
+                       $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
+               }
+               if ( isset( $server['fakeMaster'] ) ) {
+                       $db->setFakeMaster( true );
+               }
+               return $db;
+       }
+
+       function reportConnectionError( &$conn ) {
+               wfProfileIn( __METHOD__ );
+               # Prevent infinite recursion
+
+               static $reporting = false;
+               if ( !$reporting ) {
+                       $reporting = true;
+                       if ( !is_object( $conn ) ) {
+                               // No last connection, probably due to all servers being too busy
+                               $conn = new Database;
+                               if ( $this->mFailFunction ) {
+                                       $conn->failFunction( $this->mFailFunction );
+                                       $conn->reportConnectionError( $this->mLastError );
+                               } else {
+                                       // If all servers were busy, mLastError will contain something sensible
+                                       throw new DBConnectionError( $conn, $this->mLastError );
+                               }
+                       } else {
+                               if ( $this->mFailFunction ) {
+                                       $conn->failFunction( $this->mFailFunction );
+                               } else {
+                                       $conn->failFunction( false );
+                               }
+                               $server = $conn->getProperty( 'mServer' );
+                               $conn->reportConnectionError( "{$this->mLastError} ({$server})" );
+                       }
+                       $reporting = false;
+               }
+               wfProfileOut( __METHOD__ );
+       }
+
+       function getWriterIndex() {
+               return 0;
+       }
+
+       /**
+        * Returns true if the specified index is a valid server index
+        */
+       function haveIndex( $i ) {
+               return array_key_exists( $i, $this->mServers );
+       }
+
+       /**
+        * Returns true if the specified index is valid and has non-zero load
+        */
+       function isNonZeroLoad( $i ) {
+               return array_key_exists( $i, $this->mServers ) && $this->mLoads[$i] != 0;
+       }
+
+       /**
+        * Get the number of defined servers (not the number of open connections)
+        */
+       function getServerCount() {
+               return count( $this->mServers );
+       }
+
+       /**
+        * Get the host name or IP address of the server with the specified index
+        */
+       function getServerName( $i ) {
+               if ( isset( $this->mServers[$i]['hostName'] ) ) {
+                       return $this->mServers[$i]['hostName'];
+               } elseif ( isset( $this->mServers[$i]['host'] ) ) {
+                       return $this->mServers[$i]['host'];
+               } else {
+                       return '';
+               }
+       }
+
+       /**
+        * Get the current master position for chronology control purposes
+        * @return mixed
+        */
+       function getMasterPos() {
+               # If this entire request was served from a slave without opening a connection to the
+               # master (however unlikely that may be), then we can fetch the position from the slave.
+               $masterConn = $this->getAnyOpenConnection( 0 );
+               if ( !$masterConn ) {
+                       for ( $i = 1; $i < count( $this->mServers ); $i++ ) {
+                               $conn = $this->getAnyOpenConnection( $i );
+                               if ( $conn ) {
+                                       wfDebug( "Master pos fetched from slave\n" );
+                                       return $conn->getSlavePos();
+                               }
+                       }
+               } else {
+                       wfDebug( "Master pos fetched from master\n" );
+                       return $masterConn->getMasterPos();
+               }
+               return false;
+       }
+
+       /**
+        * Close all open connections
+        */
+       function closeAll() {
+               foreach ( $this->mConns as $conns2 ) {
+                       foreach  ( $conns2 as $conns3 ) {
+                               foreach ( $conns3 as $conn ) {
+                                       $conn->close();
+                               }
+                       }
+               }
+               $this->mConns = array(
+                       'local' => array(),
+                       'foreignFree' => array(),
+                       'foreignUsed' => array(),
+               );
+       }
+
+       /**
+        * Close a connection
+        * Using this function makes sure the LoadBalancer knows the connection is closed.
+        * If you use $conn->close() directly, the load balancer won't update its state.
+        */
+       function closeConnecton( $conn ) {
+               $done = false;
+               foreach ( $this->mConns as $i1 => $conns2 ) {
+                       foreach ( $conns2 as $i2 => $conns3 ) {
+                               foreach ( $conns3 as $i3 => $candidateConn ) {
+                                       if ( $conn === $candidateConn ) {
+                                               $conn->close();
+                                               unset( $this->mConns[$i1][$i2][$i3] );
+                                               $done = true;
+                                               break;
+                                       }
+                               }
+                       }
+               }
+               if ( !$done ) {
+                       $conn->close();
+               }
+       }
+
+       /**
+        * Commit transactions on all open connections
+        */
+       function commitAll() {
+               foreach ( $this->mConns as $conns2 ) {
+                       foreach ( $conns2 as $conns3 ) {
+                               foreach ( $conns3 as $conn ) {
+                                       $conn->immediateCommit();
+                               }
+                       }
+               }
+       }
+
+       /* Issue COMMIT only on master, only if queries were done on connection */
+       function commitMasterChanges() {
+               // Always 0, but who knows.. :)
+               $masterIndex = $this->getWriterIndex();
+               foreach ( $this->mConns as $type => $conns2 ) {
+                       if ( empty( $conns2[$masterIndex] ) ) {
+                               continue;
+                       }
+                       foreach ( $conns2[$masterIndex] as $conn ) {
+                               if ( $conn->lastQuery() != '' ) {
+                                       $conn->commit();
+                               }
+                       }
+               }
+       }
+
+       function waitTimeout( $value = NULL ) {
+               return wfSetVar( $this->mWaitTimeout, $value );
+       }
+
+       function getLaggedSlaveMode() {
+               return $this->mLaggedSlaveMode;
+       }
+
+       /* Disables/enables lag checks */
+       function allowLagged($mode=null) {
+               if ($mode===null)
+                       return $this->mAllowLagged;
+               $this->mAllowLagged=$mode;
+       }
+
+       function pingAll() {
+               $success = true;
+               foreach ( $this->mConns as $conns2 ) {
+                       foreach ( $conns2 as $conns3 ) {
+                               foreach ( $conns3 as $conn ) {
+                                       if ( !$conn->ping() ) {
+                                               $success = false;
+                                       }
+                               }
+                       }
+               }
+               return $success;
+       }
+
+       /**
+        * Get the hostname and lag time of the most-lagged slave.
+        * This is useful for maintenance scripts that need to throttle their updates.
+        * May attempt to open connections to slaves on the default DB.
+        */
+       function getMaxLag() {
+               $maxLag = -1;
+               $host = '';
+               foreach ( $this->mServers as $i => $conn ) {
+                       $conn = $this->getAnyOpenConnection( $i );
+                       if ( !$conn ) {
+                               $conn = $this->openConnection( $i );
+                       }
+                       if ( !$conn ) {
+                               continue;
+                       }
+                       $lag = $conn->getLag();
+                       if ( $lag > $maxLag ) {
+                               $maxLag = $lag;
+                               $host = $this->mServers[$i]['host'];
+                       }
+               }
+               return array( $host, $maxLag );
+       }
+
+       /**
+        * Get lag time for each server
+        * Results are cached for a short time in memcached, and indefinitely in the process cache
+        */
+       function getLagTimes( $wiki = false ) {
+               wfProfileIn( __METHOD__ );
+
+               if ( !isset( $this->mLagTimes ) ) {
+                       $expiry = 5;
+                       $requestRate = 10;
+
+                       global $wgMemc;
+                       $masterName = $this->getServerName( 0 );
+                       $memcKey = wfMemcKey( 'lag_times', $masterName );
+                       $times = $wgMemc->get( $memcKey );
+                       if ( $times ) {
+                               # Randomly recache with probability rising over $expiry
+                               $elapsed = time() - $times['timestamp'];
+                               $chance = max( 0, ( $expiry - $elapsed ) * $requestRate );
+                               if ( mt_rand( 0, $chance ) != 0 ) {
+                                       unset( $times['timestamp'] );
+                                       wfProfileOut( __METHOD__ );
+                                       return $times;
+                               }
+                               wfIncrStats( 'lag_cache_miss_expired' );
+                       } else {
+                               wfIncrStats( 'lag_cache_miss_absent' );
+                       }
+
+                       # Cache key missing or expired
+
+                       $times = array();
+                       foreach ( $this->mServers as $i => $conn ) {
+                               if ($i == 0) { # Master
+                                       $times[$i] = 0;
+                               } elseif ( false !== ( $conn = $this->getAnyOpenConnection( $i ) ) ) {
+                                       $times[$i] = $conn->getLag();
+                               } elseif ( false !== ( $conn = $this->openConnection( $i, $wiki ) ) ) {
+                                       $times[$i] = $conn->getLag();
+                               }
+                       }
+
+                       # Add a timestamp key so we know when it was cached
+                       $times['timestamp'] = time();
+                       $wgMemc->set( $memcKey, $times, $expiry );
+
+                       # But don't give the timestamp to the caller
+                       unset($times['timestamp']);
+                       $this->mLagTimes = $times;
+               }
+               wfProfileOut( __METHOD__ );
+               return $this->mLagTimes;
+       }
+}
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
new file mode 100644 (file)
index 0000000..d9072e9
--- /dev/null
@@ -0,0 +1,385 @@
+<?php
+
+/**
+ * Various core parser functions, registered in Parser::firstCallInit()
+ * @ingroup Parser
+ */
+class CoreParserFunctions {
+       static function register( $parser ) {
+               global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
+
+               # Syntax for arguments (see self::setFunctionHook):
+               #  "name for lookup in localized magic words array",
+               #  function callback,
+               #  optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}
+               #    instead of {{#int:...}})
+
+               $parser->setFunctionHook( 'int',              array( __CLASS__, 'intFunction'      ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'ns',               array( __CLASS__, 'ns'               ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'urlencode',        array( __CLASS__, 'urlencode'        ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'lcfirst',          array( __CLASS__, 'lcfirst'          ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'ucfirst',          array( __CLASS__, 'ucfirst'          ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'lc',               array( __CLASS__, 'lc'               ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'uc',               array( __CLASS__, 'uc'               ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'localurl',         array( __CLASS__, 'localurl'         ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'localurle',        array( __CLASS__, 'localurle'        ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'fullurl',          array( __CLASS__, 'fullurl'          ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'fullurle',         array( __CLASS__, 'fullurle'         ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'formatnum',        array( __CLASS__, 'formatnum'        ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'grammar',          array( __CLASS__, 'grammar'          ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'plural',           array( __CLASS__, 'plural'           ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'numberofpages',    array( __CLASS__, 'numberofpages'    ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'numberofusers',    array( __CLASS__, 'numberofusers'    ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'numberofarticles', array( __CLASS__, 'numberofarticles' ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'numberoffiles',    array( __CLASS__, 'numberoffiles'    ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'numberofadmins',   array( __CLASS__, 'numberofadmins'   ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'numberofedits',    array( __CLASS__, 'numberofedits'    ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'language',         array( __CLASS__, 'language'         ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'padleft',          array( __CLASS__, 'padleft'          ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'padright',         array( __CLASS__, 'padright'         ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'anchorencode',     array( __CLASS__, 'anchorencode'     ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'special',          array( __CLASS__, 'special'          ) );
+               $parser->setFunctionHook( 'defaultsort',      array( __CLASS__, 'defaultsort'      ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'filepath',         array( __CLASS__, 'filepath'         ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'pagesincategory',  array( __CLASS__, 'pagesincategory'  ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'pagesize',         array( __CLASS__, 'pagesize'         ), SFH_NO_HASH );
+               $parser->setFunctionHook( 'tag',              array( __CLASS__, 'tagObj'           ), SFH_OBJECT_ARGS );
+
+               if ( $wgAllowDisplayTitle ) {
+                       $parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
+               }
+               if ( $wgAllowSlowParserFunctions ) {
+                       $parser->setFunctionHook( 'pagesinnamespace', array( __CLASS__, 'pagesinnamespace' ), SFH_NO_HASH );
+               }
+       }
+
+       static function intFunction( $parser, $part1 = '' /*, ... */ ) {
+               if ( strval( $part1 ) !== '' ) {
+                       $args = array_slice( func_get_args(), 2 );
+                       return wfMsgReal( $part1, $args, true );
+               } else {
+                       return array( 'found' => false );
+               }
+       }
+
+       static function ns( $parser, $part1 = '' ) {
+               global $wgContLang;
+               $found = false;
+               if ( intval( $part1 ) || $part1 == "0" ) {
+                       $text = $wgContLang->getNsText( intval( $part1 ) );
+                       $found = true;
+               } else {
+                       $param = str_replace( ' ', '_', strtolower( $part1 ) );
+                       $index = MWNamespace::getCanonicalIndex( strtolower( $param ) );
+                       if ( !is_null( $index ) ) {
+                               $text = $wgContLang->getNsText( $index );
+                               $found = true;
+                       }
+               }
+               if ( $found ) {
+                       return $text;
+               } else {
+                       return array( 'found' => false );
+               }
+       }
+
+       static function urlencode( $parser, $s = '' ) {
+               return urlencode( $s );
+       }
+
+       static function lcfirst( $parser, $s = '' ) {
+               global $wgContLang;
+               return $wgContLang->lcfirst( $s );
+       }
+
+       static function ucfirst( $parser, $s = '' ) {
+               global $wgContLang;
+               return $wgContLang->ucfirst( $s );
+       }
+
+       static function lc( $parser, $s = '' ) {
+               global $wgContLang;
+               if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
+                       return $parser->markerSkipCallback( $s, array( $wgContLang, 'lc' ) );
+               } else {
+                       return $wgContLang->lc( $s );
+               }
+       }
+
+       static function uc( $parser, $s = '' ) {
+               global $wgContLang;
+               if ( is_callable( array( $parser, 'markerSkipCallback' ) ) ) {
+                       return $parser->markerSkipCallback( $s, array( $wgContLang, 'uc' ) );
+               } else {
+                       return $wgContLang->uc( $s );
+               }
+       }
+
+       static function localurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getLocalURL', $s, $arg ); }
+       static function localurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeLocalURL', $s, $arg ); }
+       static function fullurl( $parser, $s = '', $arg = null ) { return self::urlFunction( 'getFullURL', $s, $arg ); }
+       static function fullurle( $parser, $s = '', $arg = null ) { return self::urlFunction( 'escapeFullURL', $s, $arg ); }
+
+       static function urlFunction( $func, $s = '', $arg = null ) {
+               $title = Title::newFromText( $s );
+               # Due to order of execution of a lot of bits, the values might be encoded
+               # before arriving here; if that's true, then the title can't be created
+               # and the variable will fail. If we can't get a decent title from the first
+               # attempt, url-decode and try for a second.
+               if( is_null( $title ) )
+                       $title = Title::newFromUrl( urldecode( $s ) );
+               if ( !is_null( $title ) ) {
+                       if ( !is_null( $arg ) ) {
+                               $text = $title->$func( $arg );
+                       } else {
+                               $text = $title->$func();
+                       }
+                       return $text;
+               } else {
+                       return array( 'found' => false );
+               }
+       }
+
+       static function formatNum( $parser, $num = '', $raw = null) {
+               if ( self::israw( $raw ) ) {
+                       return $parser->getFunctionLang()->parseFormattedNumber( $num );
+               } else {
+                       return $parser->getFunctionLang()->formatNum( $num );
+               }
+       }
+
+       static function grammar( $parser, $case = '', $word = '' ) {
+               return $parser->getFunctionLang()->convertGrammar( $word, $case );
+       }
+
+       static function plural( $parser, $text = '') {
+               $forms = array_slice( func_get_args(), 2);
+               $text = $parser->getFunctionLang()->parseFormattedNumber( $text );
+               return $parser->getFunctionLang()->convertPlural( $text, $forms );
+       }
+
+       /**
+        * Override the title of the page when viewed, provided we've been given a
+        * title which will normalise to the canonical title
+        *
+        * @param Parser $parser Parent parser
+        * @param string $text Desired title text
+        * @return string
+        */
+       static function displaytitle( $parser, $text = '' ) {
+               $text = trim( Sanitizer::decodeCharReferences( $text ) );
+               $title = Title::newFromText( $text );
+               if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+                       $parser->mOutput->setDisplayTitle( $text );
+               return '';
+       }
+
+       static function isRaw( $param ) {
+               static $mwRaw;
+               if ( !$mwRaw ) {
+                       $mwRaw =& MagicWord::get( 'rawsuffix' );
+               }
+               if ( is_null( $param ) ) {
+                       return false;
+               } else {
+                       return $mwRaw->match( $param );
+               }
+       }
+
+       static function formatRaw( $num, $raw ) {
+               if( self::isRaw( $raw ) ) {
+                       return $num;
+               } else {
+                       global $wgContLang;
+                       return $wgContLang->formatNum( $num );
+               }
+       }
+       static function numberofpages( $parser, $raw = null ) {
+               return self::formatRaw( SiteStats::pages(), $raw );
+       }
+       static function numberofusers( $parser, $raw = null ) {
+               return self::formatRaw( SiteStats::users(), $raw );
+       }
+       static function numberofarticles( $parser, $raw = null ) {
+               return self::formatRaw( SiteStats::articles(), $raw );
+       }
+       static function numberoffiles( $parser, $raw = null ) {
+               return self::formatRaw( SiteStats::images(), $raw );
+       }
+       static function numberofadmins( $parser, $raw = null ) {
+               return self::formatRaw( SiteStats::admins(), $raw );
+       }
+       static function numberofedits( $parser, $raw = null ) {
+               return self::formatRaw( SiteStats::edits(), $raw );
+       }
+       static function pagesinnamespace( $parser, $namespace = 0, $raw = null ) {
+               return self::formatRaw( SiteStats::pagesInNs( intval( $namespace ) ), $raw );
+       }
+
+       /**
+        * Return the number of pages in the given category, or 0 if it's nonexis-
+        * tent.  This is an expensive parser function and can't be called too many
+        * times per page.
+        */
+       static function pagesincategory( $parser, $name = '', $raw = null ) {
+               static $cache = array();
+               $category = Category::newFromName( $name );
+
+               if( !is_object( $category ) ) {
+                       $cache[$name] = 0;
+                       return self::formatRaw( 0, $raw );
+               }
+
+               # Normalize name for cache
+               $name = $category->getName();
+
+               $count = 0;
+               if( isset( $cache[$name] ) ) {
+                       $count = $cache[$name];
+               } elseif( $parser->incrementExpensiveFunctionCount() ) {
+                       $count = $cache[$name] = (int)$category->getPageCount();
+               }
+               return self::formatRaw( $count, $raw );
+       }
+
+       /**
+        * Return the size of the given page, or 0 if it's nonexistent.  This is an
+        * expensive parser function and can't be called too many times per page.
+        *
+        * @FIXME This doesn't work correctly on preview for getting the size of
+        *   the current page.
+        * @FIXME Title::getLength() documentation claims that it adds things to
+        *   the link cache, so the local cache here should be unnecessary, but in
+        *   fact calling getLength() repeatedly for the same $page does seem to
+        *   run one query for each call?
+        */
+       static function pagesize( $parser, $page = '', $raw = null ) {
+               static $cache = array();
+               $title = Title::newFromText($page);
+
+               if( !is_object( $title ) ) {
+                       $cache[$page] = 0;
+                       return self::formatRaw( 0, $raw );
+               }
+
+               # Normalize name for cache
+               $page = $title->getPrefixedText();
+
+               $length = 0;
+               if( isset( $cache[$page] ) ) {
+                       $length = $cache[$page];
+               } elseif( $parser->incrementExpensiveFunctionCount() ) {
+                       $length = $cache[$page] = $title->getLength();
+       
+                       // Register dependency in templatelinks
+                       $id = $title->getArticleId();
+                       $revid = Revision::newFromTitle($title);
+                       $parser->mOutput->addTemplate($title, $id, $revid);
+               }       
+               return self::formatRaw( $length, $raw );
+       }
+
+       static function language( $parser, $arg = '' ) {
+               global $wgContLang;
+               $lang = $wgContLang->getLanguageName( strtolower( $arg ) );
+               return $lang != '' ? $lang : $arg;
+       }
+
+       static function pad( $string = '', $length = 0, $char = 0, $direction = STR_PAD_RIGHT ) {
+               $length = min( max( $length, 0 ), 500 );
+               $char = substr( $char, 0, 1 );
+               return ( $string !== '' && (int)$length > 0 && strlen( trim( (string)$char ) ) > 0 )
+                               ? str_pad( $string, $length, (string)$char, $direction )
+                               : $string;
+       }
+
+       static function padleft( $parser, $string = '', $length = 0, $char = 0 ) {
+               return self::pad( $string, $length, $char, STR_PAD_LEFT );
+       }
+
+       static function padright( $parser, $string = '', $length = 0, $char = 0 ) {
+               return self::pad( $string, $length, $char );
+       }
+
+       static function anchorencode( $parser, $text ) {
+               $a = urlencode( $text );
+               $a = strtr( $a, array( '%' => '.', '+' => '_' ) );
+               # leave colons alone, however
+               $a = str_replace( '.3A', ':', $a );
+               return $a;
+       }
+
+       static function special( $parser, $text ) {
+               $title = SpecialPage::getTitleForAlias( $text );
+               if ( $title ) {
+                       return $title->getPrefixedText();
+               } else {
+                       return wfMsgForContent( 'nosuchspecialpage' );
+               }
+       }
+
+       public static function defaultsort( $parser, $text ) {
+               $text = trim( $text );
+               if( strlen( $text ) > 0 )
+                       $parser->setDefaultSort( $text );
+               return '';
+       }
+
+       public static function filepath( $parser, $name='', $option='' ) {
+               $file = wfFindFile( $name );
+               if( $file ) {
+                       $url = $file->getFullUrl();
+                       if( $option == 'nowiki' ) {
+                               return "<nowiki>$url</nowiki>";
+                       }
+                       return $url;
+               } else {
+                       return '';
+               }
+       }
+
+       /**
+        * Parser function to extension tag adaptor
+        */
+       public static function tagObj( $parser, $frame, $args ) {
+               $xpath = false;
+               if ( !count( $args ) ) {
+                       return '';
+               }
+               $tagName = strtolower( trim( $frame->expand( array_shift( $args ) ) ) );
+
+               if ( count( $args ) ) {
+                       $inner = $frame->expand( array_shift( $args ) );
+               } else {
+                       $inner = null;
+               }
+
+               $stripList = $parser->getStripList();
+               if ( !in_array( $tagName, $stripList ) ) {
+                       return '<span class="error">' .
+                               wfMsg( 'unknown_extension_tag', $tagName ) .
+                               '</span>';
+               }
+
+               $attributes = array();
+               foreach ( $args as $arg ) {
+                       $bits = $arg->splitArg();
+                       if ( strval( $bits['index'] ) === '' ) {
+                               $name = $frame->expand( $bits['name'], PPFrame::STRIP_COMMENTS );
+                               $value = trim( $frame->expand( $bits['value'] ) );
+                               if ( preg_match( '/^(?:["\'](.+)["\']|""|\'\')$/s', $value, $m ) ) {
+                                       $value = isset( $m[1] ) ? $m[1] : '';
+                               }
+                               $attributes[$name] = $value;
+                       }
+               }
+
+               $params = array(
+                       'name' => $tagName,
+                       'inner' => $inner,
+                       'attributes' => $attributes,
+                       'close' => "</$tagName>",
+               );
+               return $parser->extensionSubstitution( $params, $frame );
+       }
+}
diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php
new file mode 100644 (file)
index 0000000..9ef11d5
--- /dev/null
@@ -0,0 +1,283 @@
+<?php
+
+/**
+ * Date formatter, recognises dates in plain text and formats them accoding to user preferences.
+ * @todo preferences, OutputPage
+ * @ingroup Parser
+ */
+class DateFormatter
+{
+       var $mSource, $mTarget;
+       var $monthNames = '', $rxDM, $rxMD, $rxDMY, $rxYDM, $rxMDY, $rxYMD;
+
+       var $regexes, $pDays, $pMonths, $pYears;
+       var $rules, $xMonths, $preferences;
+
+       const ALL = -1;
+       const NONE = 0;
+       const MDY = 1;
+       const DMY = 2;
+       const YMD = 3;
+       const ISO1 = 4;
+       const LASTPREF = 4;
+       const ISO2 = 5;
+       const YDM = 6;
+       const DM = 7;
+       const MD = 8;
+       const LAST = 8;
+
+       /**
+        * @todo document
+        */
+       function DateFormatter() {
+               global $wgContLang;
+
+               $this->monthNames = $this->getMonthRegex();
+               for ( $i=1; $i<=12; $i++ ) {
+                       $this->xMonths[$wgContLang->lc( $wgContLang->getMonthName( $i ) )] = $i;
+                       $this->xMonths[$wgContLang->lc( $wgContLang->getMonthAbbreviation( $i ) )] = $i;
+               }
+
+               $this->regexTrail = '(?![a-z])/iu';
+
+               # Partial regular expressions
+               $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')]]';
+               $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})]]';
+               $this->prxY = '\[\[(\d{1,4}([ _]BC|))]]';
+               $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})]]';
+               $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})]]';
+
+               # Real regular expressions
+               $this->regexes[self::DMY] = "/{$this->prxDM} *,? *{$this->prxY}{$this->regexTrail}";
+               $this->regexes[self::YDM] = "/{$this->prxY} *,? *{$this->prxDM}{$this->regexTrail}";
+               $this->regexes[self::MDY] = "/{$this->prxMD} *,? *{$this->prxY}{$this->regexTrail}";
+               $this->regexes[self::YMD] = "/{$this->prxY} *,? *{$this->prxMD}{$this->regexTrail}";
+               $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
+               $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
+               $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
+               $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
+
+               # Extraction keys
+               # See the comments in replace() for the meaning of the letters
+               $this->keys[self::DMY] = 'jFY';
+               $this->keys[self::YDM] = 'Y jF';
+               $this->keys[self::MDY] = 'FjY';
+               $this->keys[self::YMD] = 'Y Fj';
+               $this->keys[self::DM] = 'jF';
+               $this->keys[self::MD] = 'Fj';
+               $this->keys[self::ISO1] = 'ymd'; # y means ISO year
+               $this->keys[self::ISO2] = 'ymd';
+
+               # Target date formats
+               $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
+               $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
+               $this->targets[self::MDY] = '[[F j]], [[Y]]';
+               $this->targets[self::YMD] = '[[Y]] [[F j]]';
+               $this->targets[self::DM] = '[[F j|j F]]';
+               $this->targets[self::MD] = '[[F j]]';
+               $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
+               $this->targets[self::ISO2] = '[[y-m-d]]';
+
+               # Rules
+               #            pref    source       target
+               $this->rules[self::DMY][self::MD]       = self::DM;
+               $this->rules[self::ALL][self::MD]       = self::MD;
+               $this->rules[self::MDY][self::DM]       = self::MD;
+               $this->rules[self::ALL][self::DM]       = self::DM;
+               $this->rules[self::NONE][self::ISO2]    = self::ISO1;
+
+               $this->preferences = array(
+                       'default' => self::NONE,
+                       'dmy' => self::DMY,
+                       'mdy' => self::MDY,
+                       'ymd' => self::YMD,
+                       'ISO 8601' => self::ISO1,
+               );
+       }
+
+       /**
+        * @static
+        */
+       function &getInstance() {
+               global $wgMemc;
+               static $dateFormatter = false;
+               if ( !$dateFormatter ) {
+                       $dateFormatter = $wgMemc->get( wfMemcKey( 'dateformatter' ) );
+                       if ( !$dateFormatter ) {
+                               $dateFormatter = new DateFormatter;
+                               $wgMemc->set( wfMemcKey( 'dateformatter' ), $dateFormatter, 3600 );
+                       }
+               }
+               return $dateFormatter;
+       }
+
+       /**
+        * @param string $preference User preference
+        * @param string $text Text to reformat
+        */
+       function reformat( $preference, $text ) {
+               if ( isset( $this->preferences[$preference] ) ) {
+                       $preference = $this->preferences[$preference];
+               } else {
+                       $preference = self::NONE;
+               }
+               for ( $i=1; $i<=self::LAST; $i++ ) {
+                       $this->mSource = $i;
+                       if ( isset ( $this->rules[$preference][$i] ) ) {
+                               # Specific rules
+                               $this->mTarget = $this->rules[$preference][$i];
+                       } elseif ( isset ( $this->rules[self::ALL][$i] ) ) {
+                               # General rules
+                               $this->mTarget = $this->rules[self::ALL][$i];
+                       } elseif ( $preference ) {
+                               # User preference
+                               $this->mTarget = $preference;
+                       } else {
+                               # Default
+                               $this->mTarget = $i;
+                       }
+                       $text = preg_replace_callback( $this->regexes[$i], array( &$this, 'replace' ), $text );
+               }
+               return $text;
+       }
+
+       /**
+        * @param $matches
+        */
+       function replace( $matches ) {
+               # Extract information from $matches
+               $bits = array();
+               $key = $this->keys[$this->mSource];
+               for ( $p=0; $p < strlen($key); $p++ ) {
+                       if ( $key{$p} != ' ' ) {
+                               $bits[$key{$p}] = $matches[$p+1];
+                       }
+               }
+
+               $format = $this->targets[$this->mTarget];
+
+               # Construct new date
+               $text = '';
+               $fail = false;
+
+               for ( $p=0; $p < strlen( $format ); $p++ ) {
+                       $char = $format{$p};
+                       switch ( $char ) {
+                               case 'd': # ISO day of month
+                                       if ( !isset($bits['d']) ) {
+                                               $text .= sprintf( '%02d', $bits['j'] );
+                                       } else {
+                                               $text .= $bits['d'];
+                                       }
+                                       break;
+                               case 'm': # ISO month
+                                       if ( !isset($bits['m']) ) {
+                                               $m = $this->makeIsoMonth( $bits['F'] );
+                                               if ( !$m || $m == '00' ) {
+                                                       $fail = true;
+                                               } else {
+                                                       $text .= $m;
+                                               }
+                                       } else {
+                                               $text .= $bits['m'];
+                                       }
+                                       break;
+                               case 'y': # ISO year
+                                       if ( !isset( $bits['y'] ) ) {
+                                               $text .= $this->makeIsoYear( $bits['Y'] );
+                                       } else {
+                                               $text .= $bits['y'];
+                                       }
+                                       break;
+                               case 'j': # ordinary day of month
+                                       if ( !isset($bits['j']) ) {
+                                               $text .= intval( $bits['d'] );
+                                       } else {
+                                               $text .= $bits['j'];
+                                       }
+                                       break;
+                               case 'F': # long month
+                                       if ( !isset( $bits['F'] ) ) {
+                                               $m = intval($bits['m']);
+                                               if ( $m > 12 || $m < 1 ) {
+                                                       $fail = true;
+                                               } else {
+                                                       global $wgContLang;
+                                                       $text .= $wgContLang->getMonthName( $m );
+                                               }
+                                       } else {
+                                               $text .= ucfirst( $bits['F'] );
+                                       }
+                                       break;
+                               case 'Y': # ordinary (optional BC) year
+                                       if ( !isset( $bits['Y'] ) ) {
+                                               $text .= $this->makeNormalYear( $bits['y'] );
+                                       } else {
+                                               $text .= $bits['Y'];
+                                       }
+                                       break;
+                               default:
+                                       $text .= $char;
+                       }
+               }
+               if ( $fail ) {
+                       $text = $matches[0];
+               }
+               return $text;
+       }
+
+       /**
+        * @todo document
+        */
+       function getMonthRegex() {
+               global $wgContLang;
+               $names = array();
+               for( $i = 1; $i <= 12; $i++ ) {
+                       $names[] = $wgContLang->getMonthName( $i );
+                       $names[] = $wgContLang->getMonthAbbreviation( $i );
+               }
+               return implode( '|', $names );
+       }
+
+       /**
+        * Makes an ISO month, e.g. 02, from a month name
+        * @param $monthName String: month name
+        * @return string ISO month name
+        */
+       function makeIsoMonth( $monthName ) {
+               global $wgContLang;
+
+               $n = $this->xMonths[$wgContLang->lc( $monthName )];
+               return sprintf( '%02d', $n );
+       }
+
+       /**
+        * @todo document
+        * @param $year String: Year name
+        * @return string ISO year name
+        */
+       function makeIsoYear( $year ) {
+               # Assumes the year is in a nice format, as enforced by the regex
+               if ( substr( $year, -2 ) == 'BC' ) {
+                       $num = intval(substr( $year, 0, -3 )) - 1;
+                       # PHP bug note: sprintf( "%04d", -1 ) fails poorly
+                       $text = sprintf( '-%04d', $num );
+
+               } else {
+                       $text = sprintf( '%04d', $year );
+               }
+               return $text;
+       }
+
+       /**
+        * @todo document
+        */
+       function makeNormalYear( $iso ) {
+               if ( $iso{0} == '-' ) {
+                       $text = (intval( substr( $iso, 1 ) ) + 1) . ' BC';
+               } else {
+                       $text = intval( $iso );
+               }
+               return $text;
+       }
+}
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
new file mode 100644 (file)
index 0000000..4daa8f2
--- /dev/null
@@ -0,0 +1,4974 @@
+<?php
+/**
+ * @defgroup Parser Parser
+ *
+ * @file
+ * @ingroup Parser
+ * File for Parser and related classes
+ */
+
+
+/**
+ * PHP Parser - Processes wiki markup (which uses a more user-friendly
+ * syntax, such as "[[link]]" for making links), and provides a one-way
+ * transformation of that wiki markup it into XHTML output / markup
+ * (which in turn the browser understands, and can display).
+ *
+ * <pre>
+ * There are five main entry points into the Parser class:
+ * parse()
+ *   produces HTML output
+ * preSaveTransform().
+ *   produces altered wiki markup.
+ * preprocess()
+ *   removes HTML comments and expands templates
+ * cleanSig()
+ *   Cleans a signature before saving it to preferences
+ * extractSections()
+ *   Extracts sections from an article for section editing
+ *
+ * Globals used:
+ *    objects:   $wgLang, $wgContLang
+ *
+ * NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
+ *
+ * settings:
+ *  $wgUseTex*, $wgUseDynamicDates*, $wgInterwikiMagic*,
+ *  $wgNamespacesWithSubpages, $wgAllowExternalImages*,
+ *  $wgLocaltimezone, $wgAllowSpecialInclusion*,
+ *  $wgMaxArticleSize*
+ *
+ *  * only within ParserOptions
+ * </pre>
+ *
+ * @ingroup Parser
+ */
+class Parser
+{
+       /**
+        * Update this version number when the ParserOutput format
+        * changes in an incompatible way, so the parser cache
+        * can automatically discard old data.
+        */
+       const VERSION = '1.6.4';
+
+       # Flags for Parser::setFunctionHook
+       # Also available as global constants from Defines.php
+       const SFH_NO_HASH = 1;
+       const SFH_OBJECT_ARGS = 2;
+
+       # Constants needed for external link processing
+       # Everything except bracket, space, or control characters
+       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
+       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
+               \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
+
+       // State constants for the definition list colon extraction
+       const COLON_STATE_TEXT = 0;
+       const COLON_STATE_TAG = 1;
+       const COLON_STATE_TAGSTART = 2;
+       const COLON_STATE_CLOSETAG = 3;
+       const COLON_STATE_TAGSLASH = 4;
+       const COLON_STATE_COMMENT = 5;
+       const COLON_STATE_COMMENTDASH = 6;
+       const COLON_STATE_COMMENTDASHDASH = 7;
+
+       // Flags for preprocessToDom
+       const PTD_FOR_INCLUSION = 1;
+
+       // Allowed values for $this->mOutputType
+       // Parameter to startExternalParse().
+       const OT_HTML = 1;
+       const OT_WIKI = 2;
+       const OT_PREPROCESS = 3;
+       const OT_MSG = 3;
+
+       // Marker Suffix needs to be accessible staticly.
+       const MARKER_SUFFIX = "-QINU\x7f";
+
+       /**#@+
+        * @private
+        */
+       # Persistent:
+       var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
+               $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
+               $mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
+
+
+       # Cleared with clearState():
+       var $mOutput, $mAutonumber, $mDTopen, $mStripState;
+       var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+       var $mInterwikiLinkHolders, $mLinkHolders;
+       var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
+       var $mTplExpandCache; // empty-frame expansion cache
+       var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
+       var $mExpensiveFunctionCount; // number of expensive parser function calls
+
+       # Temporary
+       # These are variables reset at least once per parse regardless of $clearState
+       var $mOptions,      // ParserOptions object
+               $mTitle,        // Title context, used for self-link rendering and similar things
+               $mOutputType,   // Output type, one of the OT_xxx constants
+               $ot,            // Shortcut alias, see setOutputType()
+               $mRevisionId,   // ID to display in {{REVISIONID}} tags
+               $mRevisionTimestamp, // The timestamp of the specified revision ID
+               $mRevIdForTs;   // The revision ID which was used to fetch the timestamp
+
+       /**#@-*/
+
+       /**
+        * Constructor
+        *
+        * @public
+        */
+       function __construct( $conf = array() ) {
+               $this->mConf = $conf;
+               $this->mTagHooks = array();
+               $this->mTransparentTagHooks = array();
+               $this->mFunctionHooks = array();
+               $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
+               $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+               $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
+                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
+               $this->mVarCache = array();
+               if ( isset( $conf['preprocessorClass'] ) ) {
+                       $this->mPreprocessorClass = $conf['preprocessorClass'];
+               } else {
+                       $this->mPreprocessorClass = 'Preprocessor_Hash';
+               }
+               $this->mMarkerIndex = 0;
+               $this->mFirstCall = true;
+       }
+
+       /**
+        * Do various kinds of initialisation on the first call of the parser
+        */
+       function firstCallInit() {
+               if ( !$this->mFirstCall ) {
+                       return;
+               }
+               $this->mFirstCall = false;
+
+               wfProfileIn( __METHOD__ );
+
+               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+               CoreParserFunctions::register( $this );
+               $this->initialiseVariables();
+
+               wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Clear Parser state
+        *
+        * @private
+        */
+       function clearState() {
+               wfProfileIn( __METHOD__ );
+               if ( $this->mFirstCall ) {
+                       $this->firstCallInit();
+               }
+               $this->mOutput = new ParserOutput;
+               $this->mAutonumber = 0;
+               $this->mLastSection = '';
+               $this->mDTopen = false;
+               $this->mIncludeCount = array();
+               $this->mStripState = new StripState;
+               $this->mArgStack = false;
+               $this->mInPre = false;
+               $this->mInterwikiLinkHolders = array(
+                       'texts' => array(),
+                       'titles' => array()
+               );
+               $this->mLinkHolders = array(
+                       'namespaces' => array(),
+                       'dbkeys' => array(),
+                       'queries' => array(),
+                       'texts' => array(),
+                       'titles' => array()
+               );
+               $this->mRevisionTimestamp = $this->mRevisionId = null;
+
+               /**
+                * Prefix for temporary replacement strings for the multipass parser.
+                * \x07 should never appear in input as it's disallowed in XML.
+                * Using it at the front also gives us a little extra robustness
+                * since it shouldn't match when butted up against identifier-like
+                * string constructs.
+                *
+                * Must not consist of all title characters, or else it will change
+                * the behaviour of <nowiki> in a link.
+                */
+               #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
+               # Changed to \x7f to allow XML double-parsing -- TS
+               $this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+
+
+               # Clear these on every parse, bug 4549
+               $this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
+
+               $this->mShowToc = true;
+               $this->mForceTocPosition = false;
+               $this->mIncludeSizes = array(
+                       'post-expand' => 0,
+                       'arg' => 0,
+               );
+               $this->mPPNodeCount = 0;
+               $this->mDefaultSort = false;
+               $this->mHeadings = array();
+               $this->mDoubleUnderscores = array();
+               $this->mExpensiveFunctionCount = 0;
+
+               # Fix cloning
+               if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
+                       $this->mPreprocessor = null;
+               }
+
+               wfRunHooks( 'ParserClearState', array( &$this ) );
+               wfProfileOut( __METHOD__ );
+       }
+
+       function setOutputType( $ot ) {
+               $this->mOutputType = $ot;
+               // Shortcut alias
+               $this->ot = array(
+                       'html' => $ot == self::OT_HTML,
+                       'wiki' => $ot == self::OT_WIKI,
+                       'pre' => $ot == self::OT_PREPROCESS,
+               );
+       }
+
+       /**
+        * Set the context title
+        */
+       function setTitle( $t ) {
+               if ( !$t || $t instanceof FakeTitle ) {
+                       $t = Title::newFromText( 'NO TITLE' );
+               }
+               if ( strval( $t->getFragment() ) !== '' ) {
+                       # Strip the fragment to avoid various odd effects
+                       $this->mTitle = clone $t;
+                       $this->mTitle->setFragment( '' );
+               } else {
+                       $this->mTitle = $t;
+               }
+       }
+
+       /**
+        * Accessor for mUniqPrefix.
+        *
+        * @public
+        */
+       function uniqPrefix() {
+               if( !isset( $this->mUniqPrefix ) ) {
+                       // @fixme this is probably *horribly wrong*
+                       // LanguageConverter seems to want $wgParser's uniqPrefix, however
+                       // if this is called for a parser cache hit, the parser may not
+                       // have ever been initialized in the first place.
+                       // Not really sure what the heck is supposed to be going on here.
+                       return '';
+                       //throw new MWException( "Accessing uninitialized mUniqPrefix" );
+               }
+               return $this->mUniqPrefix;
+       }
+
+       /**
+        * Convert wikitext to HTML
+        * Do not call this function recursively.
+        *
+        * @param string $text Text we want to parse
+        * @param Title &$title A title object
+        * @param array $options
+        * @param boolean $linestart
+        * @param boolean $clearState
+        * @param int $revid number to pass in {{REVISIONID}}
+        * @return ParserOutput a ParserOutput
+        */
+       public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
+               /**
+                * First pass--just handle <nowiki> sections, pass the rest off
+                * to internalParse() which does all the real work.
+                */
+
+               global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
+               $fname = 'Parser::parse-' . wfGetCaller();
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( $fname );
+
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+
+               $this->mOptions = $options;
+               $this->setTitle( $title );
+               $oldRevisionId = $this->mRevisionId;
+               $oldRevisionTimestamp = $this->mRevisionTimestamp;
+               if( $revid !== null ) {
+                       $this->mRevisionId = $revid;
+                       $this->mRevisionTimestamp = null;
+               }
+               $this->setOutputType( self::OT_HTML );
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+               # No more strip!
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->internalParse( $text );
+               $text = $this->mStripState->unstripGeneral( $text );
+
+               # Clean up special characters, only run once, next-to-last before doBlockLevels
+               $fixtags = array(
+                       # french spaces, last one Guillemet-left
+                       # only if there is something before the space
+                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
+                       # french spaces, Guillemet-right
+                       '/(\\302\\253) /' => '\\1&nbsp;',
+                       '/&nbsp;(!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
+               );
+               $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
+
+               # only once and last
+               $text = $this->doBlockLevels( $text, $linestart );
+
+               $this->replaceLinkHolders( $text );
+
+               # the position of the parserConvert() call should not be changed. it
+               # assumes that the links are all replaced and the only thing left
+               # is the <nowiki> mark.
+               # Side-effects: this calls $this->mOutput->setTitleText()
+               $text = $wgContLang->parserConvert( $text, $this );
+
+               $text = $this->mStripState->unstripNoWiki( $text );
+
+               wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
+
+//!JF Move to its own function
+
+               $uniq_prefix = $this->mUniqPrefix;
+               $matches = array();
+               $elements = array_keys( $this->mTransparentTagHooks );
+               $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+               foreach( $matches as $marker => $data ) {
+                       list( $element, $content, $params, $tag ) = $data;
+                       $tagName = strtolower( $element );
+                       if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+                               $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
+                                       array( $content, $params, $this ) );
+                       } else {
+                               $output = $tag;
+                       }
+                       $this->mStripState->general->setPair( $marker, $output );
+               }
+               $text = $this->mStripState->unstripGeneral( $text );
+
+               $text = Sanitizer::normalizeCharReferences( $text );
+
+               if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
+                       $text = Parser::tidy($text);
+               } else {
+                       # attempt to sanitize at least some nesting problems
+                       # (bug #2702 and quite a few others)
+                       $tidyregs = array(
+                               # ''Something [http://www.cool.com cool''] -->
+                               # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
+                               '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
+                               '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
+                               # fix up an anchor inside another anchor, only
+                               # at least for a single single nested link (bug 3695)
+                               '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
+                               '\\1\\2</a>\\3</a>\\1\\4</a>',
+                               # fix div inside inline elements- doBlockLevels won't wrap a line which
+                               # contains a div, so fix it up here; replace
+                               # div with escaped text
+                               '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
+                               '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
+                               # remove empty italic or bold tag pairs, some
+                               # introduced by rules above
+                               '/<([bi])><\/\\1>/' => '',
+                       );
+
+                       $text = preg_replace(
+                               array_keys( $tidyregs ),
+                               array_values( $tidyregs ),
+                               $text );
+               }
+               global $wgExpensiveParserFunctionLimit;
+               if ( $this->mExpensiveFunctionCount > $wgExpensiveParserFunctionLimit ) {
+                       $this->limitationWarn( 'expensive-parserfunction', $this->mExpensiveFunctionCount, $wgExpensiveParserFunctionLimit );
+               }
+
+               wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
+
+               # Information on include size limits, for the benefit of users who try to skirt them
+               if ( $this->mOptions->getEnableLimitReport() ) {
+                       global $wgExpensiveParserFunctionLimit;
+                       $max = $this->mOptions->getMaxIncludeSize();
+                       $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
+                       $limitReport =
+                               "NewPP limit report\n" .
+                               "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
+                               "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
+                               "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
+                               $PFreport;
+                       wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
+                       $text .= "\n<!-- \n$limitReport-->\n";
+               }
+               $this->mOutput->setText( $text );
+               $this->mRevisionId = $oldRevisionId;
+               $this->mRevisionTimestamp = $oldRevisionTimestamp;
+               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
+
+               return $this->mOutput;
+       }
+
+       /**
+        * Recursive parser entry point that can be called from an extension tag
+        * hook.
+        */
+       function recursiveTagParse( $text ) {
+               wfProfileIn( __METHOD__ );
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->internalParse( $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Expand templates and variables in the text, producing valid, static wikitext.
+        * Also removes comments.
+        */
+       function preprocess( $text, $title, $options, $revid = null ) {
+               wfProfileIn( __METHOD__ );
+               $this->clearState();
+               $this->setOutputType( self::OT_PREPROCESS );
+               $this->mOptions = $options;
+               $this->setTitle( $title );
+               if( $revid !== null ) {
+                       $this->mRevisionId = $revid;
+               }
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->replaceVariables( $text );
+               $text = $this->mStripState->unstripBoth( $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Get a random string
+        *
+        * @private
+        * @static
+        */
+       function getRandomString() {
+               return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
+       }
+
+       function &getTitle() { return $this->mTitle; }
+       function getOptions() { return $this->mOptions; }
+       function getRevisionId() { return $this->mRevisionId; }
+
+       function getFunctionLang() {
+               global $wgLang, $wgContLang;
+
+               $target = $this->mOptions->getTargetLanguage();
+               if ( $target !== null ) {
+                       return $target;
+               } else {
+                       return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+               }
+       }
+
+       /**
+        * Get a preprocessor object
+        */
+       function getPreprocessor() {
+               if ( !isset( $this->mPreprocessor ) ) {
+                       $class = $this->mPreprocessorClass;
+                       $this->mPreprocessor = new $class( $this );
+               }
+               return $this->mPreprocessor;
+       }
+
+       /**
+        * Replaces all occurrences of HTML-style comments and the given tags
+        * in the text with a random marker and returns the next text. The output
+        * parameter $matches will be an associative array filled with data in
+        * the form:
+        *   'UNIQ-xxxxx' => array(
+        *     'element',
+        *     'tag content',
+        *     array( 'param' => 'x' ),
+        *     '<element param="x">tag content</element>' ) )
+        *
+        * @param $elements list of element names. Comments are always extracted.
+        * @param $text Source text string.
+        * @param $uniq_prefix
+        *
+        * @public
+        * @static
+        */
+       function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
+               static $n = 1;
+               $stripped = '';
+               $matches = array();
+
+               $taglist = implode( '|', $elements );
+               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
+
+               while ( '' != $text ) {
+                       $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
+                       $stripped .= $p[0];
+                       if( count( $p ) < 5 ) {
+                               break;
+                       }
+                       if( count( $p ) > 5 ) {
+                               // comment
+                               $element    = $p[4];
+                               $attributes = '';
+                               $close      = '';
+                               $inside     = $p[5];
+                       } else {
+                               // tag
+                               $element    = $p[1];
+                               $attributes = $p[2];
+                               $close      = $p[3];
+                               $inside     = $p[4];
+                       }
+
+                       $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
+                       $stripped .= $marker;
+
+                       if ( $close === '/>' ) {
+                               // Empty element tag, <tag />
+                               $content = null;
+                               $text = $inside;
+                               $tail = null;
+                       } else {
+                               if( $element == '!--' ) {
+                                       $end = '/(-->)/';
+                               } else {
+                                       $end = "/(<\\/$element\\s*>)/i";
+                               }
+                               $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
+                               $content = $q[0];
+                               if( count( $q ) < 3 ) {
+                                       # No end tag -- let it run out to the end of the text.
+                                       $tail = '';
+                                       $text = '';
+                               } else {
+                                       $tail = $q[1];
+                                       $text = $q[2];
+                               }
+                       }
+
+                       $matches[$marker] = array( $element,
+                               $content,
+                               Sanitizer::decodeTagAttributes( $attributes ),
+                               "<$element$attributes$close$content$tail" );
+               }
+               return $stripped;
+       }
+
+       /**
+        * Get a list of strippable XML-like elements
+        */
+       function getStripList() {
+               global $wgRawHtml;
+               $elements = $this->mStripList;
+               if( $wgRawHtml ) {
+                       $elements[] = 'html';
+               }
+               if( $this->mOptions->getUseTeX() ) {
+                       $elements[] = 'math';
+               }
+               return $elements;
+       }
+
+       /**
+        * @deprecated use replaceVariables
+        */
+       function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
+               return $text;
+       }
+
+       /**
+        * Restores pre, math, and other extensions removed by strip()
+        *
+        * always call unstripNoWiki() after this one
+        * @private
+        * @deprecated use $this->mStripState->unstrip()
+        */
+       function unstrip( $text, $state ) {
+               return $state->unstripGeneral( $text );
+       }
+
+       /**
+        * Always call this after unstrip() to preserve the order
+        *
+        * @private
+        * @deprecated use $this->mStripState->unstrip()
+        */
+       function unstripNoWiki( $text, $state ) {
+               return $state->unstripNoWiki( $text );
+       }
+
+       /**
+        * @deprecated use $this->mStripState->unstripBoth()
+        */
+       function unstripForHTML( $text ) {
+               return $this->mStripState->unstripBoth( $text );
+       }
+
+       /**
+        * Add an item to the strip state
+        * Returns the unique tag which must be inserted into the stripped text
+        * The tag will be replaced with the original text in unstrip()
+        *
+        * @private
+        */
+       function insertStripItem( $text ) {
+               $rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-" . self::MARKER_SUFFIX;
+               $this->mMarkerIndex++;
+               $this->mStripState->general->setPair( $rnd, $text );
+               return $rnd;
+       }
+
+       /**
+        * Interface with html tidy, used if $wgUseTidy = true.
+        * If tidy isn't able to correct the markup, the original will be
+        * returned in all its glory with a warning comment appended.
+        *
+        * Either the external tidy program or the in-process tidy extension
+        * will be used depending on availability. Override the default
+        * $wgTidyInternal setting to disable the internal if it's not working.
+        *
+        * @param string $text Hideous HTML input
+        * @return string Corrected HTML output
+        * @public
+        * @static
+        */
+       function tidy( $text ) {
+               global $wgTidyInternal;
+               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
+' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
+'<head><title>test</title></head><body>'.$text.'</body></html>';
+               if( $wgTidyInternal ) {
+                       $correctedtext = Parser::internalTidy( $wrappedtext );
+               } else {
+                       $correctedtext = Parser::externalTidy( $wrappedtext );
+               }
+               if( is_null( $correctedtext ) ) {
+                       wfDebug( "Tidy error detected!\n" );
+                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
+               }
+               return $correctedtext;
+       }
+
+       /**
+        * Spawn an external HTML tidy process and get corrected markup back from it.
+        *
+        * @private
+        * @static
+        */
+       function externalTidy( $text ) {
+               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
+               $fname = 'Parser::externalTidy';
+               wfProfileIn( $fname );
+
+               $cleansource = '';
+               $opts = ' -utf8';
+
+               $descriptorspec = array(
+                       0 => array('pipe', 'r'),
+                       1 => array('pipe', 'w'),
+                       2 => array('file', wfGetNull(), 'a')
+               );
+               $pipes = array();
+               $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
+               if (is_resource($process)) {
+                       // Theoretically, this style of communication could cause a deadlock
+                       // here. If the stdout buffer fills up, then writes to stdin could
+                       // block. This doesn't appear to happen with tidy, because tidy only
+                       // writes to stdout after it's finished reading from stdin. Search
+                       // for tidyParseStdin and tidySaveStdout in console/tidy.c
+                       fwrite($pipes[0], $text);
+                       fclose($pipes[0]);
+                       while (!feof($pipes[1])) {
+                               $cleansource .= fgets($pipes[1], 1024);
+                       }
+                       fclose($pipes[1]);
+                       proc_close($process);
+               }
+
+               wfProfileOut( $fname );
+
+               if( $cleansource == '' && $text != '') {
+                       // Some kind of error happened, so we couldn't get the corrected text.
+                       // Just give up; we'll use the source text and append a warning.
+                       return null;
+               } else {
+                       return $cleansource;
+               }
+       }
+
+       /**
+        * Use the HTML tidy PECL extension to use the tidy library in-process,
+        * saving the overhead of spawning a new process.
+        *
+        * 'pear install tidy' should be able to compile the extension module.
+        *
+        * @private
+        * @static
+        */
+       function internalTidy( $text ) {
+               global $wgTidyConf, $IP, $wgDebugTidy;
+               $fname = 'Parser::internalTidy';
+               wfProfileIn( $fname );
+
+               $tidy = new tidy;
+               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
+               $tidy->cleanRepair();
+               if( $tidy->getStatus() == 2 ) {
+                       // 2 is magic number for fatal error
+                       // http://www.php.net/manual/en/function.tidy-get-status.php
+                       $cleansource = null;
+               } else {
+                       $cleansource = tidy_get_output( $tidy );
+               }
+               if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
+                       $cleansource .= "<!--\nTidy reports:\n" .
+                               str_replace( '-->', '--&gt;', $tidy->errorBuffer ) .
+                               "\n-->";
+               }
+
+               wfProfileOut( $fname );
+               return $cleansource;
+       }
+
+       /**
+        * parse the wiki syntax used to render tables
+        *
+        * @private
+        */
+       function doTableStuff ( $text ) {
+               $fname = 'Parser::doTableStuff';
+               wfProfileIn( $fname );
+
+               $lines = explode ( "\n" , $text );
+               $td_history = array (); // Is currently a td tag open?
+               $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
+               $tr_history = array (); // Is currently a tr tag open?
+               $tr_attributes = array (); // history of tr attributes
+               $has_opened_tr = array(); // Did this table open a <tr> element?
+               $indent_level = 0; // indent level of the table
+               foreach ( $lines as $key => $line )
+               {
+                       $line = trim ( $line );
+
+                       if( $line == '' ) { // empty line, go to next line
+                               continue;
+                       }
+                       $first_character = $line{0};
+                       $matches = array();
+
+                       if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
+                               // First check if we are starting a new table
+                               $indent_level = strlen( $matches[1] );
+
+                               $attributes = $this->mStripState->unstripBoth( $matches[2] );
+                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
+
+                               $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
+                               array_push ( $td_history , false );
+                               array_push ( $last_tag_history , '' );
+                               array_push ( $tr_history , false );
+                               array_push ( $tr_attributes , '' );
+                               array_push ( $has_opened_tr , false );
+                       } else if ( count ( $td_history ) == 0 ) {
+                               // Don't do any of the following
+                               continue;
+                       } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
+                               // We are ending a table
+                               $line = '</table>' . substr ( $line , 2 );
+                               $last_tag = array_pop ( $last_tag_history );
+
+                               if ( !array_pop ( $has_opened_tr ) ) {
+                                       $line = "<tr><td></td></tr>{$line}";
+                               }
+
+                               if ( array_pop ( $tr_history ) ) {
+                                       $line = "</tr>{$line}";
+                               }
+
+                               if ( array_pop ( $td_history ) ) {
+                                       $line = "</{$last_tag}>{$line}";
+                               }
+                               array_pop ( $tr_attributes );
+                               $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
+                       } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
+                               // Now we have a table row
+                               $line = preg_replace( '#^\|-+#', '', $line );
+
+                               // Whats after the tag is now only attributes
+                               $attributes = $this->mStripState->unstripBoth( $line );
+                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
+                               array_pop ( $tr_attributes );
+                               array_push ( $tr_attributes , $attributes );
+
+                               $line = '';
+                               $last_tag = array_pop ( $last_tag_history );
+                               array_pop ( $has_opened_tr );
+                               array_push ( $has_opened_tr , true );
+
+                               if ( array_pop ( $tr_history ) ) {
+                                       $line = '</tr>';
+                               }
+
+                               if ( array_pop ( $td_history ) ) {
+                                       $line = "</{$last_tag}>{$line}";
+                               }
+
+                               $lines[$key] = $line;
+                               array_push ( $tr_history , false );
+                               array_push ( $td_history , false );
+                               array_push ( $last_tag_history , '' );
+                       }
+                       else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 )  == '|+' ) {
+                               // This might be cell elements, td, th or captions
+                               if ( substr ( $line , 0 , 2 ) == '|+' ) {
+                                       $first_character = '+';
+                                       $line = substr ( $line , 1 );
+                               }
+
+                               $line = substr ( $line , 1 );
+
+                               if ( $first_character == '!' ) {
+                                       $line = str_replace ( '!!' , '||' , $line );
+                               }
+
+                               // Split up multiple cells on the same line.
+                               // FIXME : This can result in improper nesting of tags processed
+                               // by earlier parser steps, but should avoid splitting up eg
+                               // attribute values containing literal "||".
+                               $cells = StringUtils::explodeMarkup( '||' , $line );
+
+                               $lines[$key] = '';
+
+                               // Loop through each table cell
+                               foreach ( $cells as $cell )
+                               {
+                                       $previous = '';
+                                       if ( $first_character != '+' )
+                                       {
+                                               $tr_after = array_pop ( $tr_attributes );
+                                               if ( !array_pop ( $tr_history ) ) {
+                                                       $previous = "<tr{$tr_after}>\n";
+                                               }
+                                               array_push ( $tr_history , true );
+                                               array_push ( $tr_attributes , '' );
+                                               array_pop ( $has_opened_tr );
+                                               array_push ( $has_opened_tr , true );
+                                       }
+
+                                       $last_tag = array_pop ( $last_tag_history );
+
+                                       if ( array_pop ( $td_history ) ) {
+                                               $previous = "</{$last_tag}>{$previous}";
+                                       }
+
+                                       if ( $first_character == '|' ) {
+                                               $last_tag = 'td';
+                                       } else if ( $first_character == '!' ) {
+                                               $last_tag = 'th';
+                                       } else if ( $first_character == '+' ) {
+                                               $last_tag = 'caption';
+                                       } else {
+                                               $last_tag = '';
+                                       }
+
+                                       array_push ( $last_tag_history , $last_tag );
+
+                                       // A cell could contain both parameters and data
+                                       $cell_data = explode ( '|' , $cell , 2 );
+
+                                       // Bug 553: Note that a '|' inside an invalid link should not
+                                       // be mistaken as delimiting cell parameters
+                                       if ( strpos( $cell_data[0], '[[' ) !== false ) {
+                                               $cell = "{$previous}<{$last_tag}>{$cell}";
+                                       } else if ( count ( $cell_data ) == 1 )
+                                               $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
+                                       else {
+                                               $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
+                                               $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
+                                               $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
+                                       }
+
+                                       $lines[$key] .= $cell;
+                                       array_push ( $td_history , true );
+                               }
+                       }
+               }
+
+               // Closing open td, tr && table
+               while ( count ( $td_history ) > 0 )
+               {
+                       if ( array_pop ( $td_history ) ) {
+                               $lines[] = '</td>' ;
+                       }
+                       if ( array_pop ( $tr_history ) ) {
+                               $lines[] = '</tr>' ;
+                       }
+                       if ( !array_pop ( $has_opened_tr ) ) {
+                               $lines[] = "<tr><td></td></tr>" ;
+                       }
+
+                       $lines[] = '</table>' ;
+               }
+
+               $output = implode ( "\n" , $lines ) ;
+
+               // special case: don't return empty table
+               if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
+                       $output = '';
+               }
+
+               wfProfileOut( $fname );
+
+               return $output;
+       }
+
+       /**
+        * Helper function for parse() that transforms wiki markup into
+        * HTML. Only called for $mOutputType == self::OT_HTML.
+        *
+        * @private
+        */
+       function internalParse( $text ) {
+               $isMain = true;
+               $fname = 'Parser::internalParse';
+               wfProfileIn( $fname );
+
+               # Hook to suspend the parser in this state
+               if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
+                       wfProfileOut( $fname );
+                       return $text ;
+               }
+
+               $text = $this->replaceVariables( $text );
+               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
+               wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
+
+               // Tables need to come after variable replacement for things to work
+               // properly; putting them before other transformations should keep
+               // exciting things like link expansions from showing up in surprising
+               // places.
+               $text = $this->doTableStuff( $text );
+
+               $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
+
+               $text = $this->doDoubleUnderscore( $text );
+               $text = $this->doHeadings( $text );
+               if($this->mOptions->getUseDynamicDates()) {
+                       $df = DateFormatter::getInstance();
+                       $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
+               }
+               $text = $this->doAllQuotes( $text );
+               $text = $this->replaceInternalLinks( $text );
+               $text = $this->replaceExternalLinks( $text );
+
+               # replaceInternalLinks may sometimes leave behind
+               # absolute URLs, which have to be masked to hide them from replaceExternalLinks
+               $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
+
+               $text = $this->doMagicLinks( $text );
+               $text = $this->formatHeadings( $text, $isMain );
+
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * Replace special strings like "ISBN xxx" and "RFC xxx" with
+        * magic external links.
+        *
+        * @private
+        */
+       function doMagicLinks( $text ) {
+               wfProfileIn( __METHOD__ );
+               $text = preg_replace_callback(
+                       '!(?:                           # Start cases
+                           <a.*?</a> |                 # Skip link text
+                           <.*?> |                     # Skip stuff inside HTML elements
+                           (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
+                           ISBN\s+(\b                  # ISBN, capture number as m[2]
+                                     (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
+                                     (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
+                                     [0-9Xx]                 # check digit
+                                   \b)
+                       )!x', array( &$this, 'magicLinkCallback' ), $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       function magicLinkCallback( $m ) {
+               if ( substr( $m[0], 0, 1 ) == '<' ) {
+                       # Skip HTML element
+                       return $m[0];
+               } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
+                       $isbn = $m[2];
+                       $num = strtr( $isbn, array(
+                               '-' => '',
+                               ' ' => '',
+                               'x' => 'X',
+                       ));
+                       $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
+                       $text = '<a href="' .
+                               $titleObj->escapeLocalUrl() .
+                               "\" class=\"internal\">ISBN $isbn</a>";
+               } else {
+                       if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
+                               $keyword = 'RFC';
+                               $urlmsg = 'rfcurl';
+                               $id = $m[1];
+                       } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
+                               $keyword = 'PMID';
+                               $urlmsg = 'pubmedurl';
+                               $id = $m[1];
+                       } else {
+                               throw new MWException( __METHOD__.': unrecognised match type "' .
+                                       substr($m[0], 0, 20 ) . '"' );
+                       }
+
+                       $url = wfMsg( $urlmsg, $id);
+                       $sk = $this->mOptions->getSkin();
+                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+                       $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+               }
+               return $text;
+       }
+
+       /**
+        * Parse headers and return html
+        *
+        * @private
+        */
+       function doHeadings( $text ) {
+               $fname = 'Parser::doHeadings';
+               wfProfileIn( $fname );
+               for ( $i = 6; $i >= 1; --$i ) {
+                       $h = str_repeat( '=', $i );
+                       $text = preg_replace( "/^$h(.+)$h\\s*$/m",
+                         "<h$i>\\1</h$i>", $text );
+               }
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * Replace single quotes with HTML markup
+        * @private
+        * @return string the altered text
+        */
+       function doAllQuotes( $text ) {
+               $fname = 'Parser::doAllQuotes';
+               wfProfileIn( $fname );
+               $outtext = '';
+               $lines = explode( "\n", $text );
+               foreach ( $lines as $line ) {
+                       $outtext .= $this->doQuotes ( $line ) . "\n";
+               }
+               $outtext = substr($outtext, 0,-1);
+               wfProfileOut( $fname );
+               return $outtext;
+       }
+
+       /**
+        * Helper function for doAllQuotes()
+        */
+       public function doQuotes( $text ) {
+               $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+               if ( count( $arr ) == 1 )
+                       return $text;
+               else
+               {
+                       # First, do some preliminary work. This may shift some apostrophes from
+                       # being mark-up to being text. It also counts the number of occurrences
+                       # of bold and italics mark-ups.
+                       $i = 0;
+                       $numbold = 0;
+                       $numitalics = 0;
+                       foreach ( $arr as $r )
+                       {
+                               if ( ( $i % 2 ) == 1 )
+                               {
+                                       # If there are ever four apostrophes, assume the first is supposed to
+                                       # be text, and the remaining three constitute mark-up for bold text.
+                                       if ( strlen( $arr[$i] ) == 4 )
+                                       {
+                                               $arr[$i-1] .= "'";
+                                               $arr[$i] = "'''";
+                                       }
+                                       # If there are more than 5 apostrophes in a row, assume they're all
+                                       # text except for the last 5.
+                                       else if ( strlen( $arr[$i] ) > 5 )
+                                       {
+                                               $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
+                                               $arr[$i] = "'''''";
+                                       }
+                                       # Count the number of occurrences of bold and italics mark-ups.
+                                       # We are not counting sequences of five apostrophes.
+                                       if ( strlen( $arr[$i] ) == 2 )      { $numitalics++;             }
+                                       else if ( strlen( $arr[$i] ) == 3 ) { $numbold++;                }
+                                       else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
+                               }
+                               $i++;
+                       }
+
+                       # If there is an odd number of both bold and italics, it is likely
+                       # that one of the bold ones was meant to be an apostrophe followed
+                       # by italics. Which one we cannot know for certain, but it is more
+                       # likely to be one that has a single-letter word before it.
+                       if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
+                       {
+                               $i = 0;
+                               $firstsingleletterword = -1;
+                               $firstmultiletterword = -1;
+                               $firstspace = -1;
+                               foreach ( $arr as $r )
+                               {
+                                       if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
+                                       {
+                                               $x1 = substr ($arr[$i-1], -1);
+                                               $x2 = substr ($arr[$i-1], -2, 1);
+                                               if ($x1 == ' ') {
+                                                       if ($firstspace == -1) $firstspace = $i;
+                                               } else if ($x2 == ' ') {
+                                                       if ($firstsingleletterword == -1) $firstsingleletterword = $i;
+                                               } else {
+                                                       if ($firstmultiletterword == -1) $firstmultiletterword = $i;
+                                               }
+                                       }
+                                       $i++;
+                               }
+
+                               # If there is a single-letter word, use it!
+                               if ($firstsingleletterword > -1)
+                               {
+                                       $arr [ $firstsingleletterword ] = "''";
+                                       $arr [ $firstsingleletterword-1 ] .= "'";
+                               }
+                               # If not, but there's a multi-letter word, use that one.
+                               else if ($firstmultiletterword > -1)
+                               {
+                                       $arr [ $firstmultiletterword ] = "''";
+                                       $arr [ $firstmultiletterword-1 ] .= "'";
+                               }
+                               # ... otherwise use the first one that has neither.
+                               # (notice that it is possible for all three to be -1 if, for example,
+                               # there is only one pentuple-apostrophe in the line)
+                               else if ($firstspace > -1)
+                               {
+                                       $arr [ $firstspace ] = "''";
+                                       $arr [ $firstspace-1 ] .= "'";
+                               }
+                       }
+
+                       # Now let's actually convert our apostrophic mush to HTML!
+                       $output = '';
+                       $buffer = '';
+                       $state = '';
+                       $i = 0;
+                       foreach ($arr as $r)
+                       {
+                               if (($i % 2) == 0)
+                               {
+                                       if ($state == 'both')
+                                               $buffer .= $r;
+                                       else
+                                               $output .= $r;
+                               }
+                               else
+                               {
+                                       if (strlen ($r) == 2)
+                                       {
+                                               if ($state == 'i')
+                                               { $output .= '</i>'; $state = ''; }
+                                               else if ($state == 'bi')
+                                               { $output .= '</i>'; $state = 'b'; }
+                                               else if ($state == 'ib')
+                                               { $output .= '</b></i><b>'; $state = 'b'; }
+                                               else if ($state == 'both')
+                                               { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
+                                               else # $state can be 'b' or ''
+                                               { $output .= '<i>'; $state .= 'i'; }
+                                       }
+                                       else if (strlen ($r) == 3)
+                                       {
+                                               if ($state == 'b')
+                                               { $output .= '</b>'; $state = ''; }
+                                               else if ($state == 'bi')
+                                               { $output .= '</i></b><i>'; $state = 'i'; }
+                                               else if ($state == 'ib')
+                                               { $output .= '</b>'; $state = 'i'; }
+                                               else if ($state == 'both')
+                                               { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
+                                               else # $state can be 'i' or ''
+                                               { $output .= '<b>'; $state .= 'b'; }
+                                       }
+                                       else if (strlen ($r) == 5)
+                                       {
+                                               if ($state == 'b')
+                                               { $output .= '</b><i>'; $state = 'i'; }
+                                               else if ($state == 'i')
+                                               { $output .= '</i><b>'; $state = 'b'; }
+                                               else if ($state == 'bi')
+                                               { $output .= '</i></b>'; $state = ''; }
+                                               else if ($state == 'ib')
+                                               { $output .= '</b></i>'; $state = ''; }
+                                               else if ($state == 'both')
+                                               { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
+                                               else # ($state == '')
+                                               { $buffer = ''; $state = 'both'; }
+                                       }
+                               }
+                               $i++;
+                       }
+                       # Now close all remaining tags.  Notice that the order is important.
+                       if ($state == 'b' || $state == 'ib')
+                               $output .= '</b>';
+                       if ($state == 'i' || $state == 'bi' || $state == 'ib')
+                               $output .= '</i>';
+                       if ($state == 'bi')
+                               $output .= '</b>';
+                       # There might be lonely ''''', so make sure we have a buffer
+                       if ($state == 'both' && $buffer)
+                               $output .= '<b><i>'.$buffer.'</i></b>';
+                       return $output;
+               }
+       }
+
+       /**
+        * Replace external links
+        *
+        * Note: this is all very hackish and the order of execution matters a lot.
+        * Make sure to run maintenance/parserTests.php if you change this code.
+        *
+        * @private
+        */
+       function replaceExternalLinks( $text ) {
+               global $wgContLang;
+               $fname = 'Parser::replaceExternalLinks';
+               wfProfileIn( $fname );
+
+               $sk = $this->mOptions->getSkin();
+
+               $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+
+               $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
+
+               $i = 0;
+               while ( $i<count( $bits ) ) {
+                       $url = $bits[$i++];
+                       $protocol = $bits[$i++];
+                       $text = $bits[$i++];
+                       $trail = $bits[$i++];
+
+                       # The characters '<' and '>' (which were escaped by
+                       # removeHTMLtags()) should not be included in
+                       # URLs, per RFC 2396.
+                       $m2 = array();
+                       if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+                               $text = substr($url, $m2[0][1]) . ' ' . $text;
+                               $url = substr($url, 0, $m2[0][1]);
+                       }
+
+                       # If the link text is an image URL, replace it with an <img> tag
+                       # This happened by accident in the original parser, but some people used it extensively
+                       $img = $this->maybeMakeExternalImage( $text );
+                       if ( $img !== false ) {
+                               $text = $img;
+                       }
+
+                       $dtrail = '';
+
+                       # Set linktype for CSS - if URL==text, link is essentially free
+                       $linktype = ($text == $url) ? 'free' : 'text';
+
+                       # No link text, e.g. [http://domain.tld/some.link]
+                       if ( $text == '' ) {
+                               # Autonumber if allowed. See bug #5918
+                               if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
+                                       $text = '[' . ++$this->mAutonumber . ']';
+                                       $linktype = 'autonumber';
+                               } else {
+                                       # Otherwise just use the URL
+                                       $text = htmlspecialchars( $url );
+                                       $linktype = 'free';
+                               }
+                       } else {
+                               # Have link text, e.g. [http://domain.tld/some.link text]s
+                               # Check for trail
+                               list( $dtrail, $trail ) = Linker::splitTrail( $trail );
+                       }
+
+                       $text = $wgContLang->markNoConversion($text);
+
+                       $url = Sanitizer::cleanUrl( $url );
+
+                       # Process the trail (i.e. everything after this link up until start of the next link),
+                       # replacing any non-bracketed links
+                       $trail = $this->replaceFreeExternalLinks( $trail );
+
+                       # Use the encoded URL
+                       # This means that users can paste URLs directly into the text
+                       # Funny characters like &ouml; aren't valid in URLs anyway
+                       # This was changed in August 2004
+                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
+
+                       # Register link in the output object.
+                       # Replace unnecessary URL escape codes with the referenced character
+                       # This prevents spammers from hiding links from the filters
+                       $pasteurized = Parser::replaceUnusualEscapes( $url );
+                       $this->mOutput->addExternalLink( $pasteurized );
+               }
+
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /**
+        * Replace anything that looks like a URL with a link
+        * @private
+        */
+       function replaceFreeExternalLinks( $text ) {
+               global $wgContLang;
+               $fname = 'Parser::replaceFreeExternalLinks';
+               wfProfileIn( $fname );
+
+               $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+               $s = array_shift( $bits );
+               $i = 0;
+
+               $sk = $this->mOptions->getSkin();
+
+               while ( $i < count( $bits ) ){
+                       $protocol = $bits[$i++];
+                       $remainder = $bits[$i++];
+
+                       $m = array();
+                       if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
+                               # Found some characters after the protocol that look promising
+                               $url = $protocol . $m[1];
+                               $trail = $m[2];
+
+                               # special case: handle urls as url args:
+                               # http://www.example.com/foo?=http://www.example.com/bar
+                               if(strlen($trail) == 0 &&
+                                       isset($bits[$i]) &&
+                                       preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
+                                       preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
+                               {
+                                       # add protocol, arg
+                                       $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
+                                       $i += 2;
+                                       $trail = $m[2];
+                               }
+
+                               # The characters '<' and '>' (which were escaped by
+                               # removeHTMLtags()) should not be included in
+                               # URLs, per RFC 2396.
+                               $m2 = array();
+                               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+                                       $trail = substr($url, $m2[0][1]) . $trail;
+                                       $url = substr($url, 0, $m2[0][1]);
+                               }
+
+                               # Move trailing punctuation to $trail
+                               $sep = ',;\.:!?';
+                               # If there is no left bracket, then consider right brackets fair game too
+                               if ( strpos( $url, '(' ) === false ) {
+                                       $sep .= ')';
+                               }
+
+                               $numSepChars = strspn( strrev( $url ), $sep );
+                               if ( $numSepChars ) {
+                                       $trail = substr( $url, -$numSepChars ) . $trail;
+                                       $url = substr( $url, 0, -$numSepChars );
+                               }
+
+                               $url = Sanitizer::cleanUrl( $url );
+
+                               # Is this an external image?
+                               $text = $this->maybeMakeExternalImage( $url );
+                               if ( $text === false ) {
+                                       # Not an image, make a link
+                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+                                       # Register it in the output object...
+                                       # Replace unnecessary URL escape codes with their equivalent characters
+                                       $pasteurized = Parser::replaceUnusualEscapes( $url );
+                                       $this->mOutput->addExternalLink( $pasteurized );
+                               }
+                               $s .= $text . $trail;
+                       } else {
+                               $s .= $protocol . $remainder;
+                       }
+               }
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /**
+        * Replace unusual URL escape codes with their equivalent characters
+        * @param string
+        * @return string
+        * @static
+        * @todo  This can merge genuinely required bits in the path or query string,
+        *        breaking legit URLs. A proper fix would treat the various parts of
+        *        the URL differently; as a workaround, just use the output for
+        *        statistical records, not for actual linking/output.
+        */
+       static function replaceUnusualEscapes( $url ) {
+               return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
+                       array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
+       }
+
+       /**
+        * Callback function used in replaceUnusualEscapes().
+        * Replaces unusual URL escape codes with their equivalent character
+        * @static
+        * @private
+        */
+       private static function replaceUnusualEscapesCallback( $matches ) {
+               $char = urldecode( $matches[0] );
+               $ord = ord( $char );
+               // Is it an unsafe or HTTP reserved character according to RFC 1738?
+               if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
+                       // No, shouldn't be escaped
+                       return $char;
+               } else {
+                       // Yes, leave it escaped
+                       return $matches[0];
+               }
+       }
+
+       /**
+        * make an image if it's allowed, either through the global
+        * option or through the exception
+        * @private
+        */
+       function maybeMakeExternalImage( $url ) {
+               $sk = $this->mOptions->getSkin();
+               $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
+               $imagesexception = !empty($imagesfrom);
+               $text = false;
+               if ( $this->mOptions->getAllowExternalImages()
+                    || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
+                       if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
+                               # Image found
+                               $text = $sk->makeExternalImage( htmlspecialchars( $url ) );
+                       }
+               }
+               return $text;
+       }
+
+       /**
+        * Process [[ ]] wikilinks
+        *
+        * @private
+        */
+       function replaceInternalLinks( $s ) {
+               global $wgContLang;
+               static $fname = 'Parser::replaceInternalLinks' ;
+
+               wfProfileIn( $fname );
+
+               wfProfileIn( $fname.'-setup' );
+               static $tc = FALSE;
+               # the % is needed to support urlencoded titles as well
+               if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
+
+               $sk = $this->mOptions->getSkin();
+
+               #split the entire text string on occurences of [[
+               $a = explode( '[[', ' ' . $s );
+               #get the first element (all text up to first [[), and remove the space we added
+               $s = array_shift( $a );
+               $s = substr( $s, 1 );
+
+               # Match a link having the form [[namespace:link|alternate]]trail
+               static $e1 = FALSE;
+               if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
+               # Match cases where there is no "]]", which might still be images
+               static $e1_img = FALSE;
+               if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
+
+               $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
+               $e2 = null;
+               if ( $useLinkPrefixExtension ) {
+                       # Match the end of a line for a word that's not followed by whitespace,
+                       # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
+                       $e2 = wfMsgForContent( 'linkprefix' );
+               }
+
+               if( is_null( $this->mTitle ) ) {
+                       wfProfileOut( $fname );
+                       wfProfileOut( $fname.'-setup' );
+                       throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
+               }
+               $nottalk = !$this->mTitle->isTalkPage();
+
+               if ( $useLinkPrefixExtension ) {
+                       $m = array();
+                       if ( preg_match( $e2, $s, $m ) ) {
+                               $first_prefix = $m[2];
+                       } else {
+                               $first_prefix = false;
+                       }
+               } else {
+                       $prefix = '';
+               }
+
+               if($wgContLang->hasVariants()) {
+                       $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
+               } else {
+                       $selflink = array($this->mTitle->getPrefixedText());
+               }
+               $useSubpages = $this->areSubpagesAllowed();
+               wfProfileOut( $fname.'-setup' );
+
+               # Loop for each link
+               for ($k = 0; isset( $a[$k] ); $k++) {
+                       $line = $a[$k];
+                       if ( $useLinkPrefixExtension ) {
+                               wfProfileIn( $fname.'-prefixhandling' );
+                               if ( preg_match( $e2, $s, $m ) ) {
+                                       $prefix = $m[2];
+                                       $s = $m[1];
+                               } else {
+                                       $prefix='';
+                               }
+                               # first link
+                               if($first_prefix) {
+                                       $prefix = $first_prefix;
+                                       $first_prefix = false;
+                               }
+                               wfProfileOut( $fname.'-prefixhandling' );
+                       }
+
+                       $might_be_img = false;
+
+                       wfProfileIn( "$fname-e1" );
+                       if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
+                               $text = $m[2];
+                               # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
+                               # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
+                               # the real problem is with the $e1 regex
+                               # See bug 1300.
+                               #
+                               # Still some problems for cases where the ] is meant to be outside punctuation,
+                               # and no image is in sight. See bug 2095.
+                               #
+                               if( $text !== '' &&
+                                       substr( $m[3], 0, 1 ) === ']' &&
+                                       strpos($text, '[') !== false
+                               )
+                               {
+                                       $text .= ']'; # so that replaceExternalLinks($text) works later
+                                       $m[3] = substr( $m[3], 1 );
+                               }
+                               # fix up urlencoded title texts
+                               if( strpos( $m[1], '%' ) !== false ) {
+                                       # Should anchors '#' also be rejected?
+                                       $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
+                               }
+                               $trail = $m[3];
+                       } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
+                               $might_be_img = true;
+                               $text = $m[2];
+                               if ( strpos( $m[1], '%' ) !== false ) {
+                                       $m[1] = urldecode($m[1]);
+                               }
+                               $trail = "";
+                       } else { # Invalid form; output directly
+                               $s .= $prefix . '[[' . $line ;
+                               wfProfileOut( "$fname-e1" );
+                               continue;
+                       }
+                       wfProfileOut( "$fname-e1" );
+                       wfProfileIn( "$fname-misc" );
+
+                       # Don't allow internal links to pages containing
+                       # PROTO: where PROTO is a valid URL protocol; these
+                       # should be external links.
+                       if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
+                               $s .= $prefix . '[[' . $line ;
+                               wfProfileOut( "$fname-misc" );
+                               continue;
+                       }
+
+                       # Make subpage if necessary
+                       if( $useSubpages ) {
+                               $link = $this->maybeDoSubpageLink( $m[1], $text );
+                       } else {
+                               $link = $m[1];
+                       }
+
+                       $noforce = (substr($m[1], 0, 1) != ':');
+                       if (!$noforce) {
+                               # Strip off leading ':'
+                               $link = substr($link, 1);
+                       }
+
+                       wfProfileOut( "$fname-misc" );
+                       wfProfileIn( "$fname-title" );
+                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
+                       if( !$nt ) {
+                               $s .= $prefix . '[[' . $line;
+                               wfProfileOut( "$fname-title" );
+                               continue;
+                       }
+
+                       $ns = $nt->getNamespace();
+                       $iw = $nt->getInterWiki();
+                       wfProfileOut( "$fname-title" );
+
+                       if ($might_be_img) { # if this is actually an invalid link
+                               wfProfileIn( "$fname-might_be_img" );
+                               if ($ns == NS_IMAGE && $noforce) { #but might be an image
+                                       $found = false;
+                                       while (isset ($a[$k+1]) ) {
+                                               #look at the next 'line' to see if we can close it there
+                                               $spliced = array_splice( $a, $k + 1, 1 );
+                                               $next_line = array_shift( $spliced );
+                                               $m = explode( ']]', $next_line, 3 );
+                                               if ( count( $m ) == 3 ) {
+                                                       # the first ]] closes the inner link, the second the image
+                                                       $found = true;
+                                                       $text .= "[[{$m[0]}]]{$m[1]}";
+                                                       $trail = $m[2];
+                                                       break;
+                                               } elseif ( count( $m ) == 2 ) {
+                                                       #if there's exactly one ]] that's fine, we'll keep looking
+                                                       $text .= "[[{$m[0]}]]{$m[1]}";
+                                               } else {
+                                                       #if $next_line is invalid too, we need look no further
+                                                       $text .= '[[' . $next_line;
+                                                       break;
+                                               }
+                                       }
+                                       if ( !$found ) {
+                                               # we couldn't find the end of this imageLink, so output it raw
+                                               #but don't ignore what might be perfectly normal links in the text we've examined
+                                               $text = $this->replaceInternalLinks($text);
+                                               $s .= "{$prefix}[[$link|$text";
+                                               # note: no $trail, because without an end, there *is* no trail
+                                               wfProfileOut( "$fname-might_be_img" );
+                                               continue;
+                                       }
+                               } else { #it's not an image, so output it raw
+                                       $s .= "{$prefix}[[$link|$text";
+                                       # note: no $trail, because without an end, there *is* no trail
+                                       wfProfileOut( "$fname-might_be_img" );
+                                       continue;
+                               }
+                               wfProfileOut( "$fname-might_be_img" );
+                       }
+
+                       $wasblank = ( '' == $text );
+                       if( $wasblank ) $text = $link;
+
+                       # Link not escaped by : , create the various objects
+                       if( $noforce ) {
+
+                               # Interwikis
+                               wfProfileIn( "$fname-interwiki" );
+                               if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+                                       $this->mOutput->addLanguageLink( $nt->getFullText() );
+                                       $s = rtrim($s . $prefix);
+                                       $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
+                                       wfProfileOut( "$fname-interwiki" );
+                                       continue;
+                               }
+                               wfProfileOut( "$fname-interwiki" );
+
+                               if ( $ns == NS_IMAGE ) {
+                                       wfProfileIn( "$fname-image" );
+                                       if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
+                                               # recursively parse links inside the image caption
+                                               # actually, this will parse them in any other parameters, too,
+                                               # but it might be hard to fix that, and it doesn't matter ATM
+                                               $text = $this->replaceExternalLinks($text);
+                                               $text = $this->replaceInternalLinks($text);
+
+                                               # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
+                                               $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
+                                               $this->mOutput->addImage( $nt->getDBkey() );
+
+                                               wfProfileOut( "$fname-image" );
+                                               continue;
+                                       } else {
+                                               # We still need to record the image's presence on the page
+                                               $this->mOutput->addImage( $nt->getDBkey() );
+                                       }
+                                       wfProfileOut( "$fname-image" );
+
+                               }
+
+                               if ( $ns == NS_CATEGORY ) {
+                                       wfProfileIn( "$fname-category" );
+                                       $s = rtrim($s . "\n"); # bug 87
+
+                                       if ( $wasblank ) {
+                                               $sortkey = $this->getDefaultSort();
+                                       } else {
+                                               $sortkey = $text;
+                                       }
+                                       $sortkey = Sanitizer::decodeCharReferences( $sortkey );
+                                       $sortkey = str_replace( "\n", '', $sortkey );
+                                       $sortkey = $wgContLang->convertCategoryKey( $sortkey );
+                                       $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
+
+                                       /**
+                                        * Strip the whitespace Category links produce, see bug 87
+                                        * @todo We might want to use trim($tmp, "\n") here.
+                                        */
+                                       $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+
+                                       wfProfileOut( "$fname-category" );
+                                       continue;
+                               }
+                       }
+
+                       # Self-link checking
+                       if( $nt->getFragment() === '' ) {
+                               if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
+                                       $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
+                                       continue;
+                               }
+                       }
+
+                       # Special and Media are pseudo-namespaces; no pages actually exist in them
+                       if( $ns == NS_MEDIA ) {
+                               # Give extensions a chance to select the file revision for us
+                               $skip = $time = false;
+                               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
+                               if ( $skip ) {
+                                       $link = $sk->makeLinkObj( $nt );
+                               } else {
+                                       $link = $sk->makeMediaLinkObj( $nt, $text, $time );
+                               }
+                               # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
+                               $s .= $prefix . $this->armorLinks( $link ) . $trail;
+                               $this->mOutput->addImage( $nt->getDBkey() );
+                               continue;
+                       } elseif( $ns == NS_SPECIAL ) {
+                               if( SpecialPage::exists( $nt->getDBkey() ) ) {
+                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+                               } else {
+                                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+                               }
+                               continue;
+                       } elseif( $ns == NS_IMAGE ) {
+                               $img = wfFindFile( $nt );
+                               if( $img ) {
+                                       // Force a blue link if the file exists; may be a remote
+                                       // upload on the shared repository, and we want to see its
+                                       // auto-generated page.
+                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+                                       $this->mOutput->addLink( $nt );
+                                       continue;
+                               }
+                       }
+                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+               }
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /**
+        * Make a link placeholder. The text returned can be later resolved to a real link with
+        * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
+        * parsing of interwiki links, and secondly to allow all existence checks and
+        * article length checks (for stub links) to be bundled into a single query.
+        *
+        */
+       function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+               wfProfileIn( __METHOD__ );
+               if ( ! is_object($nt) ) {
+                       # Fail gracefully
+                       $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
+               } else {
+                       # Separate the link trail from the rest of the link
+                       list( $inside, $trail ) = Linker::splitTrail( $trail );
+
+                       if ( $nt->isExternal() ) {
+                               $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
+                               $this->mInterwikiLinkHolders['titles'][] = $nt;
+                               $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
+                       } else {
+                               $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
+                               $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
+                               $this->mLinkHolders['queries'][] = $query;
+                               $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
+                               $this->mLinkHolders['titles'][] = $nt;
+
+                               $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               return $retVal;
+       }
+
+       /**
+        * Render a forced-blue link inline; protect against double expansion of
+        * URLs if we're in a mode that prepends full URL prefixes to internal links.
+        * Since this little disaster has to split off the trail text to avoid
+        * breaking URLs in the following text without breaking trails on the
+        * wiki links, it's been made into a horrible function.
+        *
+        * @param Title $nt
+        * @param string $text
+        * @param string $query
+        * @param string $trail
+        * @param string $prefix
+        * @return string HTML-wikitext mix oh yuck
+        */
+       function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+               list( $inside, $trail ) = Linker::splitTrail( $trail );
+               $sk = $this->mOptions->getSkin();
+               $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
+               return $this->armorLinks( $link ) . $trail;
+       }
+
+       /**
+        * Insert a NOPARSE hacky thing into any inline links in a chunk that's
+        * going to go through further parsing steps before inline URL expansion.
+        *
+        * In particular this is important when using action=render, which causes
+        * full URLs to be included.
+        *
+        * Oh man I hate our multi-layer parser!
+        *
+        * @param string more-or-less HTML
+        * @return string less-or-more HTML with NOPARSE bits
+        */
+       function armorLinks( $text ) {
+               return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
+                       "{$this->mUniqPrefix}NOPARSE$1", $text );
+       }
+
+       /**
+        * Return true if subpage links should be expanded on this page.
+        * @return bool
+        */
+       function areSubpagesAllowed() {
+               # Some namespaces don't allow subpages
+               return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
+       }
+
+       /**
+        * Handle link to subpage if necessary
+        * @param string $target the source of the link
+        * @param string &$text the link text, modified as necessary
+        * @return string the full name of the link
+        * @private
+        */
+       function maybeDoSubpageLink($target, &$text) {
+               # Valid link forms:
+               # Foobar -- normal
+               # :Foobar -- override special treatment of prefix (images, language links)
+               # /Foobar -- convert to CurrentPage/Foobar
+               # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
+               # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
+               # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
+
+               $fname = 'Parser::maybeDoSubpageLink';
+               wfProfileIn( $fname );
+               $ret = $target; # default return value is no change
+
+               # Some namespaces don't allow subpages,
+               # so only perform processing if subpages are allowed
+               if( $this->areSubpagesAllowed() ) {
+                       $hash = strpos( $target, '#' );
+                       if( $hash !== false ) {
+                               $suffix = substr( $target, $hash );
+                               $target = substr( $target, 0, $hash );
+                       } else {
+                               $suffix = '';
+                       }
+                       # bug 7425
+                       $target = trim( $target );
+                       # Look at the first character
+                       if( $target != '' && $target{0} == '/' ) {
+                               # / at end means we don't want the slash to be shown
+                               $m = array();
+                               $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
+                               if( $trailingSlashes ) {
+                                       $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
+                               } else {
+                                       $noslash = substr( $target, 1 );
+                               }
+
+                               $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
+                               if( '' === $text ) {
+                                       $text = $target . $suffix;
+                               } # this might be changed for ugliness reasons
+                       } else {
+                               # check for .. subpage backlinks
+                               $dotdotcount = 0;
+                               $nodotdot = $target;
+                               while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
+                                       ++$dotdotcount;
+                                       $nodotdot = substr( $nodotdot, 3 );
+                               }
+                               if($dotdotcount > 0) {
+                                       $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
+                                       if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
+                                               $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
+                                               # / at the end means don't show full path
+                                               if( substr( $nodotdot, -1, 1 ) == '/' ) {
+                                                       $nodotdot = substr( $nodotdot, 0, -1 );
+                                                       if( '' === $text ) {
+                                                               $text = $nodotdot . $suffix;
+                                                       }
+                                               }
+                                               $nodotdot = trim( $nodotdot );
+                                               if( $nodotdot != '' ) {
+                                                       $ret .= '/' . $nodotdot;
+                                               }
+                                               $ret .= $suffix;
+                                       }
+                               }
+                       }
+               }
+
+               wfProfileOut( $fname );
+               return $ret;
+       }
+
+       /**#@+
+        * Used by doBlockLevels()
+        * @private
+        */
+       /* private */ function closeParagraph() {
+               $result = '';
+               if ( '' != $this->mLastSection ) {
+                       $result = '</' . $this->mLastSection  . ">\n";
+               }
+               $this->mInPre = false;
+               $this->mLastSection = '';
+               return $result;
+       }
+       # getCommon() returns the length of the longest common substring
+       # of both arguments, starting at the beginning of both.
+       #
+       /* private */ function getCommon( $st1, $st2 ) {
+               $fl = strlen( $st1 );
+               $shorter = strlen( $st2 );
+               if ( $fl < $shorter ) { $shorter = $fl; }
+
+               for ( $i = 0; $i < $shorter; ++$i ) {
+                       if ( $st1{$i} != $st2{$i} ) { break; }
+               }
+               return $i;
+       }
+       # These next three functions open, continue, and close the list
+       # element appropriate to the prefix character passed into them.
+       #
+       /* private */ function openList( $char ) {
+               $result = $this->closeParagraph();
+
+               if ( '*' == $char ) { $result .= '<ul><li>'; }
+               else if ( '#' == $char ) { $result .= '<ol><li>'; }
+               else if ( ':' == $char ) { $result .= '<dl><dd>'; }
+               else if ( ';' == $char ) {
+                       $result .= '<dl><dt>';
+                       $this->mDTopen = true;
+               }
+               else { $result = '<!-- ERR 1 -->'; }
+
+               return $result;
+       }
+
+       /* private */ function nextItem( $char ) {
+               if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
+               else if ( ':' == $char || ';' == $char ) {
+                       $close = '</dd>';
+                       if ( $this->mDTopen ) { $close = '</dt>'; }
+                       if ( ';' == $char ) {
+                               $this->mDTopen = true;
+                               return $close . '<dt>';
+                       } else {
+                               $this->mDTopen = false;
+                               return $close . '<dd>';
+                       }
+               }
+               return '<!-- ERR 2 -->';
+       }
+
+       /* private */ function closeList( $char ) {
+               if ( '*' == $char ) { $text = '</li></ul>'; }
+               else if ( '#' == $char ) { $text = '</li></ol>'; }
+               else if ( ':' == $char ) {
+                       if ( $this->mDTopen ) {
+                               $this->mDTopen = false;
+                               $text = '</dt></dl>';
+                       } else {
+                               $text = '</dd></dl>';
+                       }
+               }
+               else {  return '<!-- ERR 3 -->'; }
+               return $text."\n";
+       }
+       /**#@-*/
+
+       /**
+        * Make lists from lines starting with ':', '*', '#', etc.
+        *
+        * @private
+        * @return string the lists rendered as HTML
+        */
+       function doBlockLevels( $text, $linestart ) {
+               $fname = 'Parser::doBlockLevels';
+               wfProfileIn( $fname );
+
+               # Parsing through the text line by line.  The main thing
+               # happening here is handling of block-level elements p, pre,
+               # and making lists from lines starting with * # : etc.
+               #
+               $textLines = explode( "\n", $text );
+
+               $lastPrefix = $output = '';
+               $this->mDTopen = $inBlockElem = false;
+               $prefixLength = 0;
+               $paragraphStack = false;
+
+               if ( !$linestart ) {
+                       $output .= array_shift( $textLines );
+               }
+               foreach ( $textLines as $oLine ) {
+                       $lastPrefixLength = strlen( $lastPrefix );
+                       $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
+                       $preOpenMatch = preg_match('/<pre/i', $oLine );
+                       if ( !$this->mInPre ) {
+                               # Multiple prefixes may abut each other for nested lists.
+                               $prefixLength = strspn( $oLine, '*#:;' );
+                               $pref = substr( $oLine, 0, $prefixLength );
+
+                               # eh?
+                               $pref2 = str_replace( ';', ':', $pref );
+                               $t = substr( $oLine, $prefixLength );
+                               $this->mInPre = !empty($preOpenMatch);
+                       } else {
+                               # Don't interpret any other prefixes in preformatted text
+                               $prefixLength = 0;
+                               $pref = $pref2 = '';
+                               $t = $oLine;
+                       }
+
+                       # List generation
+                       if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
+                               # Same as the last item, so no need to deal with nesting or opening stuff
+                               $output .= $this->nextItem( substr( $pref, -1 ) );
+                               $paragraphStack = false;
+
+                               if ( substr( $pref, -1 ) == ';') {
+                                       # The one nasty exception: definition lists work like this:
+                                       # ; title : definition text
+                                       # So we check for : in the remainder text to split up the
+                                       # title and definition, without b0rking links.
+                                       $term = $t2 = '';
+                                       if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+                                               $t = $t2;
+                                               $output .= $term . $this->nextItem( ':' );
+                                       }
+                               }
+                       } elseif( $prefixLength || $lastPrefixLength ) {
+                               # Either open or close a level...
+                               $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
+                               $paragraphStack = false;
+
+                               while( $commonPrefixLength < $lastPrefixLength ) {
+                                       $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
+                                       --$lastPrefixLength;
+                               }
+                               if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
+                                       $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
+                               }
+                               while ( $prefixLength > $commonPrefixLength ) {
+                                       $char = substr( $pref, $commonPrefixLength, 1 );
+                                       $output .= $this->openList( $char );
+
+                                       if ( ';' == $char ) {
+                                               # FIXME: This is dupe of code above
+                                               if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+                                                       $t = $t2;
+                                                       $output .= $term . $this->nextItem( ':' );
+                                               }
+                                       }
+                                       ++$commonPrefixLength;
+                               }
+                               $lastPrefix = $pref2;
+                       }
+                       if( 0 == $prefixLength ) {
+                               wfProfileIn( "$fname-paragraph" );
+                               # No prefix (not in list)--go to paragraph mode
+                               // XXX: use a stack for nestable elements like span, table and div
+                               $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
+                               $closematch = preg_match(
+                                       '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
+                                       '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
+                               if ( $openmatch or $closematch ) {
+                                       $paragraphStack = false;
+                                       # TODO bug 5718: paragraph closed
+                                       $output .= $this->closeParagraph();
+                                       if ( $preOpenMatch and !$preCloseMatch ) {
+                                               $this->mInPre = true;
+                                       }
+                                       if ( $closematch ) {
+                                               $inBlockElem = false;
+                                       } else {
+                                               $inBlockElem = true;
+                                       }
+                               } else if ( !$inBlockElem && !$this->mInPre ) {
+                                       if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
+                                               // pre
+                                               if ($this->mLastSection != 'pre') {
+                                                       $paragraphStack = false;
+                                                       $output .= $this->closeParagraph().'<pre>';
+                                                       $this->mLastSection = 'pre';
+                                               }
+                                               $t = substr( $t, 1 );
+                                       } else {
+                                               // paragraph
+                                               if ( '' == trim($t) ) {
+                                                       if ( $paragraphStack ) {
+                                                               $output .= $paragraphStack.'<br />';
+                                                               $paragraphStack = false;
+                                                               $this->mLastSection = 'p';
+                                                       } else {
+                                                               if ($this->mLastSection != 'p' ) {
+                                                                       $output .= $this->closeParagraph();
+                                                                       $this->mLastSection = '';
+                                                                       $paragraphStack = '<p>';
+                                                               } else {
+                                                                       $paragraphStack = '</p><p>';
+                                                               }
+                                                       }
+                                               } else {
+                                                       if ( $paragraphStack ) {
+                                                               $output .= $paragraphStack;
+                                                               $paragraphStack = false;
+                                                               $this->mLastSection = 'p';
+                                                       } else if ($this->mLastSection != 'p') {
+                                                               $output .= $this->closeParagraph().'<p>';
+                                                               $this->mLastSection = 'p';
+                                                       }
+                                               }
+                                       }
+                               }
+                               wfProfileOut( "$fname-paragraph" );
+                       }
+                       // somewhere above we forget to get out of pre block (bug 785)
+                       if($preCloseMatch && $this->mInPre) {
+                               $this->mInPre = false;
+                       }
+                       if ($paragraphStack === false) {
+                               $output .= $t."\n";
+                       }
+               }
+               while ( $prefixLength ) {
+                       $output .= $this->closeList( $pref2{$prefixLength-1} );
+                       --$prefixLength;
+               }
+               if ( '' != $this->mLastSection ) {
+                       $output .= '</' . $this->mLastSection . '>';
+                       $this->mLastSection = '';
+               }
+
+               wfProfileOut( $fname );
+               return $output;
+       }
+
+       /**
+        * Split up a string on ':', ignoring any occurences inside tags
+        * to prevent illegal overlapping.
+        * @param string $str the string to split
+        * @param string &$before set to everything before the ':'
+        * @param string &$after set to everything after the ':'
+        * return string the position of the ':', or false if none found
+        */
+       function findColonNoLinks($str, &$before, &$after) {
+               $fname = 'Parser::findColonNoLinks';
+               wfProfileIn( $fname );
+
+               $pos = strpos( $str, ':' );
+               if( $pos === false ) {
+                       // Nothing to find!
+                       wfProfileOut( $fname );
+                       return false;
+               }
+
+               $lt = strpos( $str, '<' );
+               if( $lt === false || $lt > $pos ) {
+                       // Easy; no tag nesting to worry about
+                       $before = substr( $str, 0, $pos );
+                       $after = substr( $str, $pos+1 );
+                       wfProfileOut( $fname );
+                       return $pos;
+               }
+
+               // Ugly state machine to walk through avoiding tags.
+               $state = self::COLON_STATE_TEXT;
+               $stack = 0;
+               $len = strlen( $str );
+               for( $i = 0; $i < $len; $i++ ) {
+                       $c = $str{$i};
+
+                       switch( $state ) {
+                       // (Using the number is a performance hack for common cases)
+                       case 0: // self::COLON_STATE_TEXT:
+                               switch( $c ) {
+                               case "<":
+                                       // Could be either a <start> tag or an </end> tag
+                                       $state = self::COLON_STATE_TAGSTART;
+                                       break;
+                               case ":":
+                                       if( $stack == 0 ) {
+                                               // We found it!
+                                               $before = substr( $str, 0, $i );
+                                               $after = substr( $str, $i + 1 );
+                                               wfProfileOut( $fname );
+                                               return $i;
+                                       }
+                                       // Embedded in a tag; don't break it.
+                                       break;
+                               default:
+                                       // Skip ahead looking for something interesting
+                                       $colon = strpos( $str, ':', $i );
+                                       if( $colon === false ) {
+                                               // Nothing else interesting
+                                               wfProfileOut( $fname );
+                                               return false;
+                                       }
+                                       $lt = strpos( $str, '<', $i );
+                                       if( $stack === 0 ) {
+                                               if( $lt === false || $colon < $lt ) {
+                                                       // We found it!
+                                                       $before = substr( $str, 0, $colon );
+                                                       $after = substr( $str, $colon + 1 );
+                                                       wfProfileOut( $fname );
+                                                       return $i;
+                                               }
+                                       }
+                                       if( $lt === false ) {
+                                               // Nothing else interesting to find; abort!
+                                               // We're nested, but there's no close tags left. Abort!
+                                               break 2;
+                                       }
+                                       // Skip ahead to next tag start
+                                       $i = $lt;
+                                       $state = self::COLON_STATE_TAGSTART;
+                               }
+                               break;
+                       case 1: // self::COLON_STATE_TAG:
+                               // In a <tag>
+                               switch( $c ) {
+                               case ">":
+                                       $stack++;
+                                       $state = self::COLON_STATE_TEXT;
+                                       break;
+                               case "/":
+                                       // Slash may be followed by >?
+                                       $state = self::COLON_STATE_TAGSLASH;
+                                       break;
+                               default:
+                                       // ignore
+                               }
+                               break;
+                       case 2: // self::COLON_STATE_TAGSTART:
+                               switch( $c ) {
+                               case "/":
+                                       $state = self::COLON_STATE_CLOSETAG;
+                                       break;
+                               case "!":
+                                       $state = self::COLON_STATE_COMMENT;
+                                       break;
+                               case ">":
+                                       // Illegal early close? This shouldn't happen D:
+                                       $state = self::COLON_STATE_TEXT;
+                                       break;
+                               default:
+                                       $state = self::COLON_STATE_TAG;
+                               }
+                               break;
+                       case 3: // self::COLON_STATE_CLOSETAG:
+                               // In a </tag>
+                               if( $c == ">" ) {
+                                       $stack--;
+                                       if( $stack < 0 ) {
+                                               wfDebug( "Invalid input in $fname; too many close tags\n" );
+                                               wfProfileOut( $fname );
+                                               return false;
+                                       }
+                                       $state = self::COLON_STATE_TEXT;
+                               }
+                               break;
+                       case self::COLON_STATE_TAGSLASH:
+                               if( $c == ">" ) {
+                                       // Yes, a self-closed tag <blah/>
+                                       $state = self::COLON_STATE_TEXT;
+                               } else {
+                                       // Probably we're jumping the gun, and this is an attribute
+                                       $state = self::COLON_STATE_TAG;
+                               }
+                               break;
+                       case 5: // self::COLON_STATE_COMMENT:
+                               if( $c == "-" ) {
+                                       $state = self::COLON_STATE_COMMENTDASH;
+                               }
+                               break;
+                       case self::COLON_STATE_COMMENTDASH:
+                               if( $c == "-" ) {
+                                       $state = self::COLON_STATE_COMMENTDASHDASH;
+                               } else {
+                                       $state = self::COLON_STATE_COMMENT;
+                               }
+                               break;
+                       case self::COLON_STATE_COMMENTDASHDASH:
+                               if( $c == ">" ) {
+                                       $state = self::COLON_STATE_TEXT;
+                               } else {
+                                       $state = self::COLON_STATE_COMMENT;
+                               }
+                               break;
+                       default:
+                               throw new MWException( "State machine error in $fname" );
+                       }
+               }
+               if( $stack > 0 ) {
+                       wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+                       return false;
+               }
+               wfProfileOut( $fname );
+               return false;
+       }
+
+       /**
+        * Return value of a magic variable (like PAGENAME)
+        *
+        * @private
+        */
+       function getVariableValue( $index ) {
+               global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
+
+               /**
+                * Some of these require message or data lookups and can be
+                * expensive to check many times.
+                */
+               if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
+                       if ( isset( $this->mVarCache[$index] ) ) {
+                               return $this->mVarCache[$index];
+                       }
+               }
+
+               $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
+               wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
+
+               # Use the time zone
+               global $wgLocaltimezone;
+               if ( isset( $wgLocaltimezone ) ) {
+                       $oldtz = getenv( 'TZ' );
+                       putenv( 'TZ='.$wgLocaltimezone );
+               }
+
+               wfSuppressWarnings(); // E_STRICT system time bitching
+               $localTimestamp = date( 'YmdHis', $ts );
+               $localMonth = date( 'm', $ts );
+               $localMonthName = date( 'n', $ts );
+               $localDay = date( 'j', $ts );
+               $localDay2 = date( 'd', $ts );
+               $localDayOfWeek = date( 'w', $ts );
+               $localWeek = date( 'W', $ts );
+               $localYear = date( 'Y', $ts );
+               $localHour = date( 'H', $ts );
+               if ( isset( $wgLocaltimezone ) ) {
+                       putenv( 'TZ='.$oldtz );
+               }
+               wfRestoreWarnings();
+
+               switch ( $index ) {
+                       case 'currentmonth':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+                       case 'currentmonthname':
+                               return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+                       case 'currentmonthnamegen':
+                               return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+                       case 'currentmonthabbrev':
+                               return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+                       case 'currentday':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+                       case 'currentday2':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+                       case 'localmonth':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
+                       case 'localmonthname':
+                               return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
+                       case 'localmonthnamegen':
+                               return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
+                       case 'localmonthabbrev':
+                               return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
+                       case 'localday':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
+                       case 'localday2':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 );
+                       case 'pagename':
+                               return wfEscapeWikiText( $this->mTitle->getText() );
+                       case 'pagenamee':
+                               return $this->mTitle->getPartialURL();
+                       case 'fullpagename':
+                               return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+                       case 'fullpagenamee':
+                               return $this->mTitle->getPrefixedURL();
+                       case 'subpagename':
+                               return wfEscapeWikiText( $this->mTitle->getSubpageText() );
+                       case 'subpagenamee':
+                               return $this->mTitle->getSubpageUrlForm();
+                       case 'basepagename':
+                               return wfEscapeWikiText( $this->mTitle->getBaseText() );
+                       case 'basepagenamee':
+                               return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+                       case 'talkpagename':
+                               if( $this->mTitle->canTalk() ) {
+                                       $talkPage = $this->mTitle->getTalkPage();
+                                       return wfEscapeWikiText( $talkPage->getPrefixedText() );
+                               } else {
+                                       return '';
+                               }
+                       case 'talkpagenamee':
+                               if( $this->mTitle->canTalk() ) {
+                                       $talkPage = $this->mTitle->getTalkPage();
+                                       return $talkPage->getPrefixedUrl();
+                               } else {
+                                       return '';
+                               }
+                       case 'subjectpagename':
+                               $subjPage = $this->mTitle->getSubjectPage();
+                               return wfEscapeWikiText( $subjPage->getPrefixedText() );
+                       case 'subjectpagenamee':
+                               $subjPage = $this->mTitle->getSubjectPage();
+                               return $subjPage->getPrefixedUrl();
+                       case 'revisionid':
+                               // Let the edit saving system know we should parse the page
+                               // *after* a revision ID has been assigned.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
+                               return $this->mRevisionId;
+                       case 'revisionday':
+                               // Let the edit saving system know we should parse the page
+                               // *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
+                               return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+                       case 'revisionday2':
+                               // Let the edit saving system know we should parse the page
+                               // *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
+                               return substr( $this->getRevisionTimestamp(), 6, 2 );
+                       case 'revisionmonth':
+                               // Let the edit saving system know we should parse the page
+                               // *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
+                               return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+                       case 'revisionyear':
+                               // Let the edit saving system know we should parse the page
+                               // *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
+                               return substr( $this->getRevisionTimestamp(), 0, 4 );
+                       case 'revisiontimestamp':
+                               // Let the edit saving system know we should parse the page
+                               // *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
+                               return $this->getRevisionTimestamp();
+                       case 'namespace':
+                               return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                       case 'namespacee':
+                               return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                       case 'talkspace':
+                               return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
+                       case 'talkspacee':
+                               return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+                       case 'subjectspace':
+                               return $this->mTitle->getSubjectNsText();
+                       case 'subjectspacee':
+                               return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+                       case 'currentdayname':
+                               return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+                       case 'currentyear':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+                       case 'currenttime':
+                               return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+                       case 'currenthour':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+                       case 'currentweek':
+                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+                               // int to remove the padding
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+                       case 'currentdow':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+                       case 'localdayname':
+                               return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+                       case 'localyear':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
+                       case 'localtime':
+                               return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
+                       case 'localhour':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localHour, true );
+                       case 'localweek':
+                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+                               // int to remove the padding
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek );
+                       case 'localdow':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
+                       case 'numberofarticles':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
+                       case 'numberoffiles':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
+                       case 'numberofusers':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+                       case 'numberofpages':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
+                       case 'numberofadmins':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() );
+                       case 'numberofedits':
+                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+                       case 'currenttimestamp':
+                               return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
+                       case 'localtimestamp':
+                               return $this->mVarCache[$index] = $localTimestamp;
+                       case 'currentversion':
+                               return $this->mVarCache[$index] = SpecialVersion::getVersion();
+                       case 'sitename':
+                               return $wgSitename;
+                       case 'server':
+                               return $wgServer;
+                       case 'servername':
+                               return $wgServerName;
+                       case 'scriptpath':
+                               return $wgScriptPath;
+                       case 'directionmark':
+                               return $wgContLang->getDirMark();
+                       case 'contentlanguage':
+                               global $wgContLanguageCode;
+                               return $wgContLanguageCode;
+                       default:
+                               $ret = null;
+                               if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) )
+                                       return $ret;
+                               else
+                                       return null;
+               }
+       }
+
+       /**
+        * initialise the magic variables (like CURRENTMONTHNAME)
+        *
+        * @private
+        */
+       function initialiseVariables() {
+               $fname = 'Parser::initialiseVariables';
+               wfProfileIn( $fname );
+               $variableIDs = MagicWord::getVariableIDs();
+
+               $this->mVariables = new MagicWordArray( $variableIDs );
+               wfProfileOut( $fname );
+       }
+
+       /**
+        * Preprocess some wikitext and return the document tree.
+        * This is the ghost of replace_variables().
+        *
+        * @param string $text The text to parse
+        * @param integer flags Bitwise combination of:
+        *          self::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
+        *                                     included. Default is to assume a direct page view.
+        *
+        * The generated DOM tree must depend only on the input text and the flags.
+        * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+        *
+        * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+        * change in the DOM tree for a given text, must be passed through the section identifier
+        * in the section edit link and thus back to extractSections().
+        *
+        * The output of this function is currently only cached in process memory, but a persistent
+        * cache may be implemented at a later date which takes further advantage of these strict
+        * dependency requirements.
+        *
+        * @private
+        */
+       function preprocessToDom ( $text, $flags = 0 ) {
+               $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
+               return $dom;
+       }
+
+       /*
+        * Return a three-element array: leading whitespace, string contents, trailing whitespace
+        */
+       public static function splitWhitespace( $s ) {
+               $ltrimmed = ltrim( $s );
+               $w1 = substr( $s, 0, strlen( $s ) - strlen( $ltrimmed ) );
+               $trimmed = rtrim( $ltrimmed );
+               $diff = strlen( $ltrimmed ) - strlen( $trimmed );
+               if ( $diff > 0 ) {
+                       $w2 = substr( $ltrimmed, -$diff );
+               } else {
+                       $w2 = '';
+               }
+               return array( $w1, $trimmed, $w2 );
+       }
+
+       /**
+        * Replace magic variables, templates, and template arguments
+        * with the appropriate text. Templates are substituted recursively,
+        * taking care to avoid infinite loops.
+        *
+        * Note that the substitution depends on value of $mOutputType:
+        *  self::OT_WIKI: only {{subst:}} templates
+        *  self::OT_PREPROCESS: templates but not extension tags
+        *  self::OT_HTML: all templates and extension tags
+        *
+        * @param string $tex The text to transform
+        * @param PPFrame $frame Object describing the arguments passed to the template
+        * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
+        * @private
+        */
+       function replaceVariables( $text, $frame = false, $argsOnly = false ) {
+               # Prevent too big inclusions
+               if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
+                       return $text;
+               }
+
+               $fname = __METHOD__;
+               wfProfileIn( $fname );
+
+               if ( $frame === false ) {
+                       $frame = $this->getPreprocessor()->newFrame();
+               } elseif ( !( $frame instanceof PPFrame ) ) {
+                       throw new MWException( __METHOD__ . ' called using the old argument format' );
+               }
+
+               $dom = $this->preprocessToDom( $text );
+               $flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
+               $text = $frame->expand( $dom, $flags );
+
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
+       static function createAssocArgs( $args ) {
+               $assocArgs = array();
+               $index = 1;
+               foreach( $args as $arg ) {
+                       $eqpos = strpos( $arg, '=' );
+                       if ( $eqpos === false ) {
+                               $assocArgs[$index++] = $arg;
+                       } else {
+                               $name = trim( substr( $arg, 0, $eqpos ) );
+                               $value = trim( substr( $arg, $eqpos+1 ) );
+                               if ( $value === false ) {
+                                       $value = '';
+                               }
+                               if ( $name !== false ) {
+                                       $assocArgs[$name] = $value;
+                               }
+                       }
+               }
+
+               return $assocArgs;
+       }
+
+       /**
+        * Warn the user when a parser limitation is reached
+        * Will warn at most once the user per limitation type
+        *
+        * @param string $limitationType, should be one of:
+        *   'expensive-parserfunction' (corresponding messages: 'expensive-parserfunction-warning', 'expensive-parserfunction-category')
+        *   'post-expand-template-argument' (corresponding messages: 'post-expand-template-argument-warning', 'post-expand-template-argument-category')
+        *   'post-expand-template-inclusion' (corresponding messages: 'post-expand-template-inclusion-warning', 'post-expand-template-inclusion-category')
+        * @params int $current, $max When an explicit limit has been
+        *       exceeded, provide the values (optional)
+        */
+       function limitationWarn( $limitationType, $current=null, $max=null) {
+               $msgName = $limitationType . '-warning';
+               //does no harm if $current and $max are present but are unnecessary for the message
+               $warning = wfMsg( $msgName, $current, $max); 
+               $this->mOutput->addWarning( $warning );
+               $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
+               if ( $cat ) {
+                       $this->mOutput->addCategory( $cat->getDBkey(), $this->getDefaultSort() );
+               }
+       }
+
+       /**
+        * Return the text of a template, after recursively
+        * replacing any variables or templates within the template.
+        *
+        * @param array $piece The parts of the template
+        *  $piece['title']: the title, i.e. the part before the |
+        *  $piece['parts']: the parameter array
+        *  $piece['lineStart']: whether the brace was at the start of a line
+        * @param PPFrame The current frame, contains template arguments
+        * @return string the text of the template
+        * @private
+        */
+       function braceSubstitution( $piece, $frame ) {
+               global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
+               $fname = __METHOD__;
+               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__.'-setup' );
+
+               # Flags
+               $found = false;             # $text has been filled
+               $nowiki = false;            # wiki markup in $text should be escaped
+               $isHTML = false;            # $text is HTML, armour it against wikitext transformation
+               $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
+               $isChildObj = false;        # $text is a DOM node needing expansion in a child frame
+               $isLocalObj = false;        # $text is a DOM node needing expansion in the current frame
+
+               # Title object, where $text came from
+               $title = NULL;
+
+               # $part1 is the bit before the first |, and must contain only title characters.
+               # Various prefixes will be stripped from it later.
+               $titleWithSpaces = $frame->expand( $piece['title'] );
+               $part1 = trim( $titleWithSpaces );
+               $titleText = false;
+
+               # Original title text preserved for various purposes
+               $originalTitle = $part1;
+
+               # $args is a list of argument nodes, starting from index 0, not including $part1
+               $args = (null == $piece['parts']) ? array() : $piece['parts'];
+               wfProfileOut( __METHOD__.'-setup' );
+
+               # SUBST
+               wfProfileIn( __METHOD__.'-modifiers' );
+               if ( !$found ) {
+                       $mwSubst = MagicWord::get( 'subst' );
+                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
+                               # One of two possibilities is true:
+                               # 1) Found SUBST but not in the PST phase
+                               # 2) Didn't find SUBST and in the PST phase
+                               # In either case, return without further processing
+                               $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
+                               $isLocalObj = true;
+                               $found = true;
+                       }
+               }
+
+               # Variables
+               if ( !$found && $args->getLength() == 0 ) {
+                       $id = $this->mVariables->matchStartToEnd( $part1 );
+                       if ( $id !== false ) {
+                               $text = $this->getVariableValue( $id );
+                               if (MagicWord::getCacheTTL($id)>-1)
+                                       $this->mOutput->mContainsOldMagic = true;
+                               $found = true;
+                       }
+               }
+
+               # MSG, MSGNW and RAW
+               if ( !$found ) {
+                       # Check for MSGNW:
+                       $mwMsgnw = MagicWord::get( 'msgnw' );
+                       if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
+                               $nowiki = true;
+                       } else {
+                               # Remove obsolete MSG:
+                               $mwMsg = MagicWord::get( 'msg' );
+                               $mwMsg->matchStartAndRemove( $part1 );
+                       }
+
+                       # Check for RAW:
+                       $mwRaw = MagicWord::get( 'raw' );
+                       if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
+                               $forceRawInterwiki = true;
+                       }
+               }
+               wfProfileOut( __METHOD__.'-modifiers' );
+
+               # Parser functions
+               if ( !$found ) {
+                       wfProfileIn( __METHOD__ . '-pfunc' );
+
+                       $colonPos = strpos( $part1, ':' );
+                       if ( $colonPos !== false ) {
+                               # Case sensitive functions
+                               $function = substr( $part1, 0, $colonPos );
+                               if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
+                                       $function = $this->mFunctionSynonyms[1][$function];
+                               } else {
+                                       # Case insensitive functions
+                                       $function = strtolower( $function );
+                                       if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
+                                               $function = $this->mFunctionSynonyms[0][$function];
+                                       } else {
+                                               $function = false;
+                                       }
+                               }
+                               if ( $function ) {
+                                       list( $callback, $flags ) = $this->mFunctionHooks[$function];
+                                       $initialArgs = array( &$this );
+                                       $funcArgs = array( trim( substr( $part1, $colonPos + 1 ) ) );
+                                       if ( $flags & SFH_OBJECT_ARGS ) {
+                                               # Add a frame parameter, and pass the arguments as an array
+                                               $allArgs = $initialArgs;
+                                               $allArgs[] = $frame;
+                                               for ( $i = 0; $i < $args->getLength(); $i++ ) {
+                                                       $funcArgs[] = $args->item( $i );
+                                               }
+                                               $allArgs[] = $funcArgs;
+                                       } else {
+                                               # Convert arguments to plain text
+                                               for ( $i = 0; $i < $args->getLength(); $i++ ) {
+                                                       $funcArgs[] = trim( $frame->expand( $args->item( $i ) ) );
+                                               }
+                                               $allArgs = array_merge( $initialArgs, $funcArgs );
+                                       }
+
+                                       # Workaround for PHP bug 35229 and similar
+                                       if ( !is_callable( $callback ) ) {
+                                               throw new MWException( "Tag hook for $name is not callable\n" );
+                                       }
+                                       $result = call_user_func_array( $callback, $allArgs );
+                                       $found = true;
+                                       $noparse = true;
+                                       $preprocessFlags = 0;
+                                       
+                                       if ( is_array( $result ) ) {
+                                               if ( isset( $result[0] ) ) {
+                                                       $text = $result[0];
+                                                       unset( $result[0] );
+                                               }
+
+                                               // Extract flags into the local scope
+                                               // This allows callers to set flags such as nowiki, found, etc.
+                                               extract( $result );
+                                       } else {
+                                               $text = $result;
+                                       }
+                                       if ( !$noparse ) {
+                                               $text = $this->preprocessToDom( $text, $preprocessFlags );
+                                               $isChildObj = true;
+                                       }
+                               }
+                       }
+                       wfProfileOut( __METHOD__ . '-pfunc' );
+               }
+
+               # Finish mangling title and then check for loops.
+               # Set $title to a Title object and $titleText to the PDBK
+               if ( !$found ) {
+                       $ns = NS_TEMPLATE;
+                       # Split the title into page and subpage
+                       $subpage = '';
+                       $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
+                       if ($subpage !== '') {
+                               $ns = $this->mTitle->getNamespace();
+                       }
+                       $title = Title::newFromText( $part1, $ns );
+                       if ( $title ) {
+                               $titleText = $title->getPrefixedText();
+                               # Check for language variants if the template is not found
+                               if($wgContLang->hasVariants() && $title->getArticleID() == 0){
+                                       $wgContLang->findVariantLink($part1, $title);
+                               }
+                               # Do infinite loop check
+                               if ( !$frame->loopCheck( $title ) ) {
+                                       $found = true;
+                                       $text = "<span class=\"error\">Template loop detected: [[$titleText]]</span>";
+                                       wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
+                               }
+                               # Do recursion depth check
+                               $limit = $this->mOptions->getMaxTemplateDepth();
+                               if ( $frame->depth >= $limit ) {
+                                       $found = true;
+                                       $text = "<span class=\"error\">Template recursion depth limit exceeded ($limit)</span>";
+                               }
+                       }
+               }
+
+               # Load from database
+               if ( !$found && $title ) {
+                       wfProfileIn( __METHOD__ . '-loadtpl' );
+                       if ( !$title->isExternal() ) {
+                               if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
+                                       $text = SpecialPage::capturePath( $title );
+                                       if ( is_string( $text ) ) {
+                                               $found = true;
+                                               $isHTML = true;
+                                               $this->disableCache();
+                                       }
+                               } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
+                                       $found = false; //access denied
+                                       wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
+                               } else {
+                                       list( $text, $title ) = $this->getTemplateDom( $title );
+                                       if ( $text !== false ) {
+                                               $found = true;
+                                               $isChildObj = true;
+                                       }
+                               }
+
+                               # If the title is valid but undisplayable, make a link to it
+                               if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+                                       $text = "[[:$titleText]]";
+                                       $found = true;
+                               }
+                       } elseif ( $title->isTrans() ) {
+                               // Interwiki transclusion
+                               if ( $this->ot['html'] && !$forceRawInterwiki ) {
+                                       $text = $this->interwikiTransclude( $title, 'render' );
+                                       $isHTML = true;
+                               } else {
+                                       $text = $this->interwikiTransclude( $title, 'raw' );
+                                       // Preprocess it like a template
+                                       $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
+                                       $isChildObj = true;
+                               }
+                               $found = true;
+                       }
+                       wfProfileOut( __METHOD__ . '-loadtpl' );
+               }
+
+               # If we haven't found text to substitute by now, we're done
+               # Recover the source wikitext and return it
+               if ( !$found ) {
+                       $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
+                       wfProfileOut( $fname );
+                       return array( 'object' => $text );
+               }
+
+               # Expand DOM-style return values in a child frame
+               if ( $isChildObj ) {
+                       # Clean up argument array
+                       $newFrame = $frame->newChild( $args, $title );
+
+                       if ( $nowiki ) {
+                               $text = $newFrame->expand( $text, PPFrame::RECOVER_ORIG );
+                       } elseif ( $titleText !== false && $newFrame->isEmpty() ) {
+                               # Expansion is eligible for the empty-frame cache
+                               if ( isset( $this->mTplExpandCache[$titleText] ) ) {
+                                       $text = $this->mTplExpandCache[$titleText];
+                               } else {
+                                       $text = $newFrame->expand( $text );
+                                       $this->mTplExpandCache[$titleText] = $text;
+                               }
+                       } else {
+                               # Uncached expansion
+                               $text = $newFrame->expand( $text );
+                       }
+               }
+               if ( $isLocalObj && $nowiki ) {
+                       $text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
+                       $isLocalObj = false;
+               }
+
+               # Replace raw HTML by a placeholder
+               # Add a blank line preceding, to prevent it from mucking up
+               # immediately preceding headings
+               if ( $isHTML ) {
+                       $text = "\n\n" . $this->insertStripItem( $text );
+               }
+               # Escape nowiki-style return values
+               elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+                       $text = wfEscapeWikiText( $text );
+               }
+               # Bug 529: if the template begins with a table or block-level
+               # element, it should be treated as beginning a new line.
+               # This behaviour is somewhat controversial.
+               elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
+                       $text = "\n" . $text;
+               }
+
+               if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
+                       # Error, oversize inclusion
+                       $text = "[[$originalTitle]]" .
+                               $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
+                       $this->limitationWarn( 'post-expand-template-inclusion' );
+               }
+
+               if ( $isLocalObj ) {
+                       $ret = array( 'object' => $text );
+               } else {
+                       $ret = array( 'text' => $text );
+               }
+
+               wfProfileOut( $fname );
+               return $ret;
+       }
+
+       /**
+        * Get the semi-parsed DOM representation of a template with a given title,
+        * and its redirect destination title. Cached.
+        */
+       function getTemplateDom( $title ) {
+               $cacheTitle = $title;
+               $titleText = $title->getPrefixedDBkey();
+
+               if ( isset( $this->mTplRedirCache[$titleText] ) ) {
+                       list( $ns, $dbk ) = $this->mTplRedirCache[$titleText];
+                       $title = Title::makeTitle( $ns, $dbk );
+                       $titleText = $title->getPrefixedDBkey();
+               }
+               if ( isset( $this->mTplDomCache[$titleText] ) ) {
+                       return array( $this->mTplDomCache[$titleText], $title );
+               }
+
+               // Cache miss, go to the database
+               list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
+
+               if ( $text === false ) {
+                       $this->mTplDomCache[$titleText] = false;
+                       return array( false, $title );
+               }
+
+               $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
+               $this->mTplDomCache[ $titleText ] = $dom;
+
+               if (! $title->equals($cacheTitle)) {
+                       $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
+                               array( $title->getNamespace(),$cdb = $title->getDBkey() );
+               }
+
+               return array( $dom, $title );
+       }
+
+       /**
+        * Fetch the unparsed text of a template and register a reference to it.
+        */
+       function fetchTemplateAndTitle( $title ) {
+               $templateCb = $this->mOptions->getTemplateCallback();
+               $stuff = call_user_func( $templateCb, $title, $this );
+               $text = $stuff['text'];
+               $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
+               if ( isset( $stuff['deps'] ) ) {
+                       foreach ( $stuff['deps'] as $dep ) {
+                               $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
+                       }
+               }
+               return array($text,$finalTitle);
+       }
+
+       function fetchTemplate( $title ) {
+               $rv = $this->fetchTemplateAndTitle($title);
+               return $rv[0];
+       }
+
+       /**
+        * Static function to get a template
+        * Can be overridden via ParserOptions::setTemplateCallback().
+        */
+       static function statelessFetchTemplate( $title, $parser=false ) {
+               $text = $skip = false;
+               $finalTitle = $title;
+               $deps = array();
+
+               // Loop to fetch the article, with up to 1 redirect
+               for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
+                       # Give extensions a chance to select the revision instead
+                       $id = false; // Assume current
+                       wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
+
+                       if( $skip ) {
+                               $text = false;
+                               $deps[] = array(
+                                       'title' => $title,
+                                       'page_id' => $title->getArticleID(),
+                                       'rev_id' => null );
+                               break;
+                       }
+                       $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
+                       $rev_id = $rev ? $rev->getId() : 0;
+                       // If there is no current revision, there is no page
+                       if( $id === false && !$rev ) {
+                               $linkCache = LinkCache::singleton();
+                               $linkCache->addBadLinkObj( $title );
+                       }
+
+                       $deps[] = array(
+                               'title' => $title,
+                               'page_id' => $title->getArticleID(),
+                               'rev_id' => $rev_id );
+
+                       if( $rev ) {
+                               $text = $rev->getText();
+                       } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
+                               global $wgLang;
+                               $message = $wgLang->lcfirst( $title->getText() );
+                               $text = wfMsgForContentNoTrans( $message );
+                               if( wfEmptyMsg( $message, $text ) ) {
+                                       $text = false;
+                                       break;
+                               }
+                       } else {
+                               break;
+                       }
+                       if ( $text === false ) {
+                               break;
+                       }
+                       // Redirect?
+                       $finalTitle = $title;
+                       $title = Title::newFromRedirect( $text );
+               }
+               return array(
+                       'text' => $text,
+                       'finalTitle' => $finalTitle,
+                       'deps' => $deps );
+       }
+
+       /**
+        * Transclude an interwiki link.
+        */
+       function interwikiTransclude( $title, $action ) {
+               global $wgEnableScaryTranscluding;
+
+               if (!$wgEnableScaryTranscluding)
+                       return wfMsg('scarytranscludedisabled');
+
+               $url = $title->getFullUrl( "action=$action" );
+
+               if (strlen($url) > 255)
+                       return wfMsg('scarytranscludetoolong');
+               return $this->fetchScaryTemplateMaybeFromCache($url);
+       }
+
+       function fetchScaryTemplateMaybeFromCache($url) {
+               global $wgTranscludeCacheExpiry;
+               $dbr = wfGetDB(DB_SLAVE);
+               $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
+                               array('tc_url' => $url));
+               if ($obj) {
+                       $time = $obj->tc_time;
+                       $text = $obj->tc_contents;
+                       if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
+                               return $text;
+                       }
+               }
+
+               $text = Http::get($url);
+               if (!$text)
+                       return wfMsg('scarytranscludefailed', $url);
+
+               $dbw = wfGetDB(DB_MASTER);
+               $dbw->replace('transcache', array('tc_url'), array(
+                       'tc_url' => $url,
+                       'tc_time' => time(),
+                       'tc_contents' => $text));
+               return $text;
+       }
+
+
+       /**
+        * Triple brace replacement -- used for template arguments
+        * @private
+        */
+       function argSubstitution( $piece, $frame ) {
+               wfProfileIn( __METHOD__ );
+
+               $error = false;
+               $parts = $piece['parts'];
+               $nameWithSpaces = $frame->expand( $piece['title'] );
+               $argName = trim( $nameWithSpaces );
+               $object = false;
+               $text = $frame->getArgument( $argName );
+               if (  $text === false && $parts->getLength() > 0
+                 && (
+                   $this->ot['html']
+                   || $this->ot['pre']
+                   || ( $this->ot['wiki'] && $frame->isTemplate() )
+                 )
+               ) {
+                       # No match in frame, use the supplied default
+                       $object = $parts->item( 0 )->getChildren();
+               }
+               if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
+                       $error = '<!-- WARNING: argument omitted, expansion size too large -->';
+                       $this->limitationWarn( 'post-expand-template-argument' );
+               }
+
+               if ( $text === false && $object === false ) {
+                       # No match anywhere
+                       $object = $frame->virtualBracketedImplode( '{{{', '|', '}}}', $nameWithSpaces, $parts );
+               }
+               if ( $error !== false ) {
+                       $text .= $error;
+               }
+               if ( $object !== false ) {
+                       $ret = array( 'object' => $object );
+               } else {
+                       $ret = array( 'text' => $text );
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $ret;
+       }
+
+       /**
+        * Return the text to be used for a given extension tag.
+        * This is the ghost of strip().
+        *
+        * @param array $params Associative array of parameters:
+        *     name       PPNode for the tag name
+        *     attr       PPNode for unparsed text where tag attributes are thought to be
+        *     attributes Optional associative array of parsed attributes
+        *     inner      Contents of extension element
+        *     noClose    Original text did not have a close tag
+        * @param PPFrame $frame
+        */
+       function extensionSubstitution( $params, $frame ) {
+               global $wgRawHtml, $wgContLang;
+
+               $name = $frame->expand( $params['name'] );
+               $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
+               $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
+
+               $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
+
+               if ( $this->ot['html'] ) {
+                       $name = strtolower( $name );
+
+                       $attributes = Sanitizer::decodeTagAttributes( $attrText );
+                       if ( isset( $params['attributes'] ) ) {
+                               $attributes = $attributes + $params['attributes'];
+                       }
+                       switch ( $name ) {
+                               case 'html':
+                                       if( $wgRawHtml ) {
+                                               $output = $content;
+                                               break;
+                                       } else {
+                                               throw new MWException( '<html> extension tag encountered unexpectedly' );
+                                       }
+                               case 'nowiki':
+                                       $output = Xml::escapeTagsOnly( $content );
+                                       break;
+                               case 'math':
+                                       $output = $wgContLang->armourMath(
+                                               MathRenderer::renderMath( $content, $attributes ) );
+                                       break;
+                               case 'gallery':
+                                       $output = $this->renderImageGallery( $content, $attributes );
+                                       break;
+                               default:
+                                       if( isset( $this->mTagHooks[$name] ) ) {
+                                               # Workaround for PHP bug 35229 and similar
+                                               if ( !is_callable( $this->mTagHooks[$name] ) ) {
+                                                       throw new MWException( "Tag hook for $name is not callable\n" );
+                                               }
+                                               $output = call_user_func_array( $this->mTagHooks[$name],
+                                                       array( $content, $attributes, $this ) );
+                                       } else {
+                                               $output = '<span class="error">Invalid tag extension name: ' .
+                                                       htmlspecialchars( $name ) . '</span>';
+                                       }
+                       }
+               } else {
+                       if ( is_null( $attrText ) ) {
+                               $attrText = '';
+                       }
+                       if ( isset( $params['attributes'] ) ) {
+                               foreach ( $params['attributes'] as $attrName => $attrValue ) {
+                                       $attrText .= ' ' . htmlspecialchars( $attrName ) . '="' .
+                                               htmlspecialchars( $attrValue ) . '"';
+                               }
+                       }
+                       if ( $content === null ) {
+                               $output = "<$name$attrText/>";
+                       } else {
+                               $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] );
+                               $output = "<$name$attrText>$content$close";
+                       }
+               }
+
+               if ( $name == 'html' || $name == 'nowiki' ) {
+                       $this->mStripState->nowiki->setPair( $marker, $output );
+               } else {
+                       $this->mStripState->general->setPair( $marker, $output );
+               }
+               return $marker;
+       }
+
+       /**
+        * Increment an include size counter
+        *
+        * @param string $type The type of expansion
+        * @param integer $size The size of the text
+        * @return boolean False if this inclusion would take it over the maximum, true otherwise
+        */
+       function incrementIncludeSize( $type, $size ) {
+               if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) {
+                       return false;
+               } else {
+                       $this->mIncludeSizes[$type] += $size;
+                       return true;
+               }
+       }
+
+       /**
+        * Increment the expensive function count
+        *
+        * @return boolean False if the limit has been exceeded
+        */
+       function incrementExpensiveFunctionCount() {
+               global $wgExpensiveParserFunctionLimit;
+               $this->mExpensiveFunctionCount++;
+               if($this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit) {
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * Strip double-underscore items like __NOGALLERY__ and __NOTOC__
+        * Fills $this->mDoubleUnderscores, returns the modified text
+        */
+       function doDoubleUnderscore( $text ) {
+               // The position of __TOC__ needs to be recorded
+               $mw = MagicWord::get( 'toc' );
+               if( $mw->match( $text ) ) {
+                       $this->mShowToc = true;
+                       $this->mForceTocPosition = true;
+
+                       // Set a placeholder. At the end we'll fill it in with the TOC.
+                       $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
+
+                       // Only keep the first one.
+                       $text = $mw->replace( '', $text );
+               }
+
+               // Now match and remove the rest of them
+               $mwa = MagicWord::getDoubleUnderscoreArray();
+               $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
+
+               if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
+                       $this->mOutput->mNoGallery = true;
+               }
+               if ( isset( $this->mDoubleUnderscores['notoc'] ) && !$this->mForceTocPosition ) {
+                       $this->mShowToc = false;
+               }
+               if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
+                       $this->mOutput->setProperty( 'hiddencat', 'y' );
+
+                       $containerCategory = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( 'hidden-category-category' ) );
+                       if ( $containerCategory ) {
+                               $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
+                       } else {
+                               wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" );
+                       }
+               }
+               return $text;
+       }
+
+       /**
+        * This function accomplishes several tasks:
+        * 1) Auto-number headings if that option is enabled
+        * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
+        * 3) Add a Table of contents on the top for users who have enabled the option
+        * 4) Auto-anchor headings
+        *
+        * It loops through all headlines, collects the necessary data, then splits up the
+        * string and re-inserts the newly formatted headlines.
+        *
+        * @param string $text
+        * @param boolean $isMain
+        * @private
+        */
+       function formatHeadings( $text, $isMain=true ) {
+               global $wgMaxTocLevel, $wgContLang;
+
+               $doNumberHeadings = $this->mOptions->getNumberHeadings();
+               if( !$this->mTitle->quickUserCan( 'edit' ) ) {
+                       $showEditLink = 0;
+               } else {
+                       $showEditLink = $this->mOptions->getEditSection();
+               }
+
+               # Inhibit editsection links if requested in the page
+               if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
+                       $showEditLink = 0;
+               }
+
+               # Get all headlines for numbering them and adding funky stuff like [edit]
+               # links - this is for later, but we need the number of headlines right now
+               $matches = array();
+               $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
+
+               # if there are fewer than 4 headlines in the article, do not show TOC
+               # unless it's been explicitly enabled.
+               $enoughToc = $this->mShowToc &&
+                       (($numMatches >= 4) || $this->mForceTocPosition);
+
+               # Allow user to stipulate that a page should have a "new section"
+               # link added via __NEWSECTIONLINK__
+               if ( isset( $this->mDoubleUnderscores['newsectionlink'] ) ) {
+                       $this->mOutput->setNewSection( true );
+               }
+
+               # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
+               # override above conditions and always show TOC above first header
+               if ( isset( $this->mDoubleUnderscores['forcetoc'] ) ) {
+                       $this->mShowToc = true;
+                       $enoughToc = true;
+               }
+
+               # We need this to perform operations on the HTML
+               $sk = $this->mOptions->getSkin();
+
+               # headline counter
+               $headlineCount = 0;
+               $numVisible = 0;
+
+               # Ugh .. the TOC should have neat indentation levels which can be
+               # passed to the skin functions. These are determined here
+               $toc = '';
+               $full = '';
+               $head = array();
+               $sublevelCount = array();
+               $levelCount = array();
+               $toclevel = 0;
+               $level = 0;
+               $prevlevel = 0;
+               $toclevel = 0;
+               $prevtoclevel = 0;
+               $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
+               $baseTitleText = $this->mTitle->getPrefixedDBkey();
+               $tocraw = array();
+
+               foreach( $matches[3] as $headline ) {
+                       $isTemplate = false;
+                       $titleText = false;
+                       $sectionIndex = false;
+                       $numbering = '';
+                       $markerMatches = array();
+                       if (preg_match("/^$markerRegex/", $headline, $markerMatches)) {
+                               $serial = $markerMatches[1];
+                               list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
+                               $isTemplate = ($titleText != $baseTitleText);
+                               $headline = preg_replace("/^$markerRegex/", "", $headline);
+                       }
+
+                       if( $toclevel ) {
+                               $prevlevel = $level;
+                               $prevtoclevel = $toclevel;
+                       }
+                       $level = $matches[1][$headlineCount];
+
+                       if( $doNumberHeadings || $enoughToc ) {
+
+                               if ( $level > $prevlevel ) {
+                                       # Increase TOC level
+                                       $toclevel++;
+                                       $sublevelCount[$toclevel] = 0;
+                                       if( $toclevel<$wgMaxTocLevel ) {
+                                               $prevtoclevel = $toclevel;
+                                               $toc .= $sk->tocIndent();
+                                               $numVisible++;
+                                       }
+                               }
+                               elseif ( $level < $prevlevel && $toclevel > 1 ) {
+                                       # Decrease TOC level, find level to jump to
+
+                                       if ( $toclevel == 2 && $level <= $levelCount[1] ) {
+                                               # Can only go down to level 1
+                                               $toclevel = 1;
+                                       } else {
+                                               for ($i = $toclevel; $i > 0; $i--) {
+                                                       if ( $levelCount[$i] == $level ) {
+                                                               # Found last matching level
+                                                               $toclevel = $i;
+                                                               break;
+                                                       }
+                                                       elseif ( $levelCount[$i] < $level ) {
+                                                               # Found first matching level below current level
+                                                               $toclevel = $i + 1;
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                                       if( $toclevel<$wgMaxTocLevel ) {
+                                               if($prevtoclevel < $wgMaxTocLevel) {
+                                                       # Unindent only if the previous toc level was shown :p
+                                                       $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+                                                       $prevtoclevel = $toclevel;
+                                               } else {
+                                                       $toc .= $sk->tocLineEnd();
+                                               }
+                                       }
+                               }
+                               else {
+                                       # No change in level, end TOC line
+                                       if( $toclevel<$wgMaxTocLevel ) {
+                                               $toc .= $sk->tocLineEnd();
+                                       }
+                               }
+
+                               $levelCount[$toclevel] = $level;
+
+                               # count number of headlines for each level
+                               @$sublevelCount[$toclevel]++;
+                               $dot = 0;
+                               for( $i = 1; $i <= $toclevel; $i++ ) {
+                                       if( !empty( $sublevelCount[$i] ) ) {
+                                               if( $dot ) {
+                                                       $numbering .= '.';
+                                               }
+                                               $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
+                                               $dot = 1;
+                                       }
+                               }
+                       }
+
+                       # The safe header is a version of the header text safe to use for links
+                       # Avoid insertion of weird stuff like <math> by expanding the relevant sections
+                       $safeHeadline = $this->mStripState->unstripBoth( $headline );
+
+                       # Remove link placeholders by the link text.
+                       #     <!--LINK number-->
+                       # turns into
+                       #     link text with suffix
+                       $safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
+                                                           "\$this->mLinkHolders['texts'][\$1]",
+                                                           $safeHeadline );
+                       $safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
+                                                           "\$this->mInterwikiLinkHolders['texts'][\$1]",
+                                                           $safeHeadline );
+
+                       # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
+                       $tocline = preg_replace(
+                               array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
+                               array( '',                          '<$1>'),
+                               $safeHeadline
+                       );
+                       $tocline = trim( $tocline );
+
+                       # For the anchor, strip out HTML-y stuff period
+                       $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
+                       $safeHeadline = trim( $safeHeadline );
+
+                       # Save headline for section edit hint before it's escaped
+                       $headlineHint = $safeHeadline;
+                       $safeHeadline = Sanitizer::escapeId( $safeHeadline );
+                       # HTML names must be case-insensitively unique (bug 10721)
+                       $arrayKey = strtolower( $safeHeadline );
+
+                       # count how many in assoc. array so we can track dupes in anchors
+                       isset( $refers[$arrayKey] ) ? $refers[$arrayKey]++ : $refers[$arrayKey] = 1;
+                       $refcount[$headlineCount] = $refers[$arrayKey];
+
+                       # Don't number the heading if it is the only one (looks silly)
+                       if( $doNumberHeadings && count( $matches[3] ) > 1) {
+                               # the two are different if the line contains a link
+                               $headline=$numbering . ' ' . $headline;
+                       }
+
+                       # Create the anchor for linking from the TOC to the section
+                       $anchor = $safeHeadline;
+                       if($refcount[$headlineCount] > 1 ) {
+                               $anchor .= '_' . $refcount[$headlineCount];
+                       }
+                       if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
+                               $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
+                               $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
+                       }
+                       # give headline the correct <h#> tag
+                       if( $showEditLink && $sectionIndex !== false ) {
+                               if( $isTemplate ) {
+                                       # Put a T flag in the section identifier, to indicate to extractSections()
+                                       # that sections inside <includeonly> should be counted.
+                                       $editlink = $sk->editSectionLinkForOther($titleText, "T-$sectionIndex");
+                               } else {
+                                       $editlink = $sk->editSectionLink($this->mTitle, $sectionIndex, $headlineHint);
+                               }
+                       } else {
+                               $editlink = '';
+                       }
+                       $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
+
+                       $headlineCount++;
+               }
+
+               $this->mOutput->setSections( $tocraw );
+
+               # Never ever show TOC if no headers
+               if( $numVisible < 1 ) {
+                       $enoughToc = false;
+               }
+
+               if( $enoughToc ) {
+                       if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
+                               $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
+                       }
+                       $toc = $sk->tocList( $toc );
+               }
+
+               # split up and insert constructed headlines
+
+               $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
+               $i = 0;
+
+               foreach( $blocks as $block ) {
+                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
+                               # This is the [edit] link that appears for the top block of text when
+                               # section editing is enabled
+
+                               # Disabled because it broke block formatting
+                               # For example, a bullet point in the top line
+                               # $full .= $sk->editSectionLink(0);
+                       }
+                       $full .= $block;
+                       if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
+                               # Top anchor now in skin
+                               $full = $full.$toc;
+                       }
+
+                       if( !empty( $head[$i] ) ) {
+                               $full .= $head[$i];
+                       }
+                       $i++;
+               }
+               if( $this->mForceTocPosition ) {
+                       return str_replace( '<!--MWTOC-->', $toc, $full );
+               } else {
+                       return $full;
+               }
+       }
+
+       /**
+        * Transform wiki markup when saving a page by doing \r\n -> \n
+        * conversion, substitting signatures, {{subst:}} templates, etc.
+        *
+        * @param string $text the text to transform
+        * @param Title &$title the Title object for the current article
+        * @param User &$user the User object describing the current user
+        * @param ParserOptions $options parsing options
+        * @param bool $clearState whether to clear the parser state first
+        * @return string the altered wiki markup
+        * @public
+        */
+       function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
+               $this->mOptions = $options;
+               $this->setTitle( $title );
+               $this->setOutputType( self::OT_WIKI );
+
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+
+               $pairs = array(
+                       "\r\n" => "\n",
+               );
+               $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
+               $text = $this->pstPass2( $text, $user );
+               $text = $this->mStripState->unstripBoth( $text );
+               return $text;
+       }
+
+       /**
+        * Pre-save transform helper function
+        * @private
+        */
+       function pstPass2( $text, $user ) {
+               global $wgContLang, $wgLocaltimezone;
+
+               /* Note: This is the timestamp saved as hardcoded wikitext to
+                * the database, we use $wgContLang here in order to give
+                * everyone the same signature and use the default one rather
+                * than the one selected in each user's preferences.
+                *
+                * (see also bug 12815)
+                */
+               $ts = $this->mOptions->getTimestamp();
+               $tz = 'UTC';
+               if ( isset( $wgLocaltimezone ) ) {
+                       $unixts = wfTimestamp( TS_UNIX, $ts );
+                       $oldtz = getenv( 'TZ' );
+                       putenv( 'TZ='.$wgLocaltimezone );
+                       $ts = date( 'YmdHis', $unixts );
+                       $tz = date( 'T', $unixts );  # might vary on DST changeover!
+                       putenv( 'TZ='.$oldtz );
+               }
+               $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)";
+
+               # Variable replacement
+               # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+               $text = $this->replaceVariables( $text );
+
+               # Signatures
+               $sigText = $this->getUserSig( $user );
+               $text = strtr( $text, array(
+                       '~~~~~' => $d,
+                       '~~~~' => "$sigText $d",
+                       '~~~' => $sigText
+               ) );
+
+               # Context links: [[|name]] and [[name (context)|]]
+               #
+               global $wgLegalTitleChars;
+               $tc = "[$wgLegalTitleChars]";
+               $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
+
+               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/";            # [[ns:page (context)|]]
+               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/";  # [[ns:page (context), context|]]
+               $p2 = "/\[\[\\|($tc+)]]/";                                      # [[|page]]
+
+               # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
+               $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
+               $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
+
+               $t = $this->mTitle->getText();
+               $m = array();
+               if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
+                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
+               } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
+                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
+               } else {
+                       # if there's no context, don't bother duplicating the title
+                       $text = preg_replace( $p2, '[[\\1]]', $text );
+               }
+
+               # Trim trailing whitespace
+               $text = rtrim( $text );
+
+               return $text;
+       }
+
+       /**
+        * Fetch the user's signature text, if any, and normalize to
+        * validated, ready-to-insert wikitext.
+        *
+        * @param User $user
+        * @return string
+        * @private
+        */
+       function getUserSig( &$user ) {
+               global $wgMaxSigChars;
+
+               $username = $user->getName();
+               $nickname = $user->getOption( 'nickname' );
+               $nickname = $nickname === '' ? $username : $nickname;
+
+               if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
+                       $nickname = $username;
+                       wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
+               } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
+                       # Sig. might contain markup; validate this
+                       if( $this->validateSig( $nickname ) !== false ) {
+                               # Validated; clean up (if needed) and return it
+                               return $this->cleanSig( $nickname, true );
+                       } else {
+                               # Failed to validate; fall back to the default
+                               $nickname = $username;
+                               wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+                       }
+               }
+
+               // Make sure nickname doesnt get a sig in a sig
+               $nickname = $this->cleanSigInSig( $nickname );
+
+               # If we're still here, make it a link to the user page
+               $userText = wfEscapeWikiText( $username );
+               $nickText = wfEscapeWikiText( $nickname );
+               if ( $user->isAnon() )  {
+                       return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
+               } else {
+                       return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
+               }
+       }
+
+       /**
+        * Check that the user's signature contains no bad XML
+        *
+        * @param string $text
+        * @return mixed An expanded string, or false if invalid.
+        */
+       function validateSig( $text ) {
+               return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+       }
+
+       /**
+        * Clean up signature text
+        *
+        * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
+        * 2) Substitute all transclusions
+        *
+        * @param string $text
+        * @param $parsing Whether we're cleaning (preferences save) or parsing
+        * @return string Signature text
+        */
+       function cleanSig( $text, $parsing = false ) {
+               if ( !$parsing ) {
+                       global $wgTitle;
+                       $this->clearState();
+                       $this->setTitle( $wgTitle );
+                       $this->mOptions = new ParserOptions;
+                       $this->setOutputType = self::OT_PREPROCESS;
+               }
+
+               # FIXME: regex doesn't respect extension tags or nowiki
+               #  => Move this logic to braceSubstitution()
+               $substWord = MagicWord::get( 'subst' );
+               $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
+               $substText = '{{' . $substWord->getSynonym( 0 );
+
+               $text = preg_replace( $substRegex, $substText, $text );
+               $text = $this->cleanSigInSig( $text );
+               $dom = $this->preprocessToDom( $text );
+               $frame = $this->getPreprocessor()->newFrame();
+               $text = $frame->expand( $dom );
+
+               if ( !$parsing ) {
+                       $text = $this->mStripState->unstripBoth( $text );
+               }
+
+               return $text;
+       }
+
+       /**
+        * Strip ~~~, ~~~~ and ~~~~~ out of signatures
+        * @param string $text
+        * @return string Signature text with /~{3,5}/ removed
+        */
+       function cleanSigInSig( $text ) {
+               $text = preg_replace( '/~{3,5}/', '', $text );
+               return $text;
+       }
+
+       /**
+        * Set up some variables which are usually set up in parse()
+        * so that an external function can call some class members with confidence
+        * @public
+        */
+       function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
+               $this->setTitle( $title );
+               $this->mOptions = $options;
+               $this->setOutputType( $outputType );
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+       }
+
+       /**
+        * Wrapper for preprocess()
+        *
+        * @param string $text the text to preprocess
+        * @param ParserOptions $options  options
+        * @return string
+        * @public
+        */
+       function transformMsg( $text, $options ) {
+               global $wgTitle;
+               static $executing = false;
+
+               $fname = "Parser::transformMsg";
+
+               # Guard against infinite recursion
+               if ( $executing ) {
+                       return $text;
+               }
+               $executing = true;
+
+               wfProfileIn($fname);
+               $text = $this->preprocess( $text, $wgTitle, $options );
+
+               $executing = false;
+               wfProfileOut($fname);
+               return $text;
+       }
+
+       /**
+        * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
+        * The callback should have the following form:
+        *    function myParserHook( $text, $params, &$parser ) { ... }
+        *
+        * Transform and return $text. Use $parser for any required context, e.g. use
+        * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
+        *
+        * @public
+        *
+        * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
+        * @param mixed $callback The callback function (and object) to use for the tag
+        *
+        * @return The old value of the mTagHooks array associated with the hook
+        */
+       function setHook( $tag, $callback ) {
+               $tag = strtolower( $tag );
+               $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
+               $this->mTagHooks[$tag] = $callback;
+               if( !in_array( $tag, $this->mStripList ) ) {
+                       $this->mStripList[] = $tag;
+               }
+
+               return $oldVal;
+       }
+
+       function setTransparentTagHook( $tag, $callback ) {
+               $tag = strtolower( $tag );
+               $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
+               $this->mTransparentTagHooks[$tag] = $callback;
+
+               return $oldVal;
+       }
+
+       /**
+        * Remove all tag hooks
+        */
+       function clearTagHooks() {
+               $this->mTagHooks = array();
+               $this->mStripList = $this->mDefaultStripList;
+       }
+
+       /**
+        * Create a function, e.g. {{sum:1|2|3}}
+        * The callback function should have the form:
+        *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
+        *
+        * The callback may either return the text result of the function, or an array with the text
+        * in element 0, and a number of flags in the other elements. The names of the flags are
+        * specified in the keys. Valid flags are:
+        *   found                     The text returned is valid, stop processing the template. This
+        *                             is on by default.
+        *   nowiki                    Wiki markup in the return value should be escaped
+        *   isHTML                    The returned text is HTML, armour it against wikitext transformation
+        *
+        * @public
+        *
+        * @param string $id The magic word ID
+        * @param mixed $callback The callback function (and object) to use
+        * @param integer $flags a combination of the following flags:
+        *                SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
+        *
+        * @return The old callback function for this name, if any
+        */
+       function setFunctionHook( $id, $callback, $flags = 0 ) {
+               $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
+               $this->mFunctionHooks[$id] = array( $callback, $flags );
+
+               # Add to function cache
+               $mw = MagicWord::get( $id );
+               if( !$mw )
+                       throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
+
+               $synonyms = $mw->getSynonyms();
+               $sensitive = intval( $mw->isCaseSensitive() );
+
+               foreach ( $synonyms as $syn ) {
+                       # Case
+                       if ( !$sensitive ) {
+                               $syn = strtolower( $syn );
+                       }
+                       # Add leading hash
+                       if ( !( $flags & SFH_NO_HASH ) ) {
+                               $syn = '#' . $syn;
+                       }
+                       # Remove trailing colon
+                       if ( substr( $syn, -1, 1 ) == ':' ) {
+                               $syn = substr( $syn, 0, -1 );
+                       }
+                       $this->mFunctionSynonyms[$sensitive][$syn] = $id;
+               }
+               return $oldVal;
+       }
+
+       /**
+        * Get all registered function hook identifiers
+        *
+        * @return array
+        */
+       function getFunctionHooks() {
+               return array_keys( $this->mFunctionHooks );
+       }
+
+       /**
+        * Replace <!--LINK--> link placeholders with actual links, in the buffer
+        * Placeholders created in Skin::makeLinkObj()
+        * Returns an array of link CSS classes, indexed by PDBK.
+        * $options is a bit field, RLH_FOR_UPDATE to select for update
+        */
+       function replaceLinkHolders( &$text, $options = 0 ) {
+               global $wgUser;
+               global $wgContLang;
+
+               $fname = 'Parser::replaceLinkHolders';
+               wfProfileIn( $fname );
+
+               $pdbks = array();
+               $colours = array();
+               $linkcolour_ids = array();
+               $sk = $this->mOptions->getSkin();
+               $linkCache = LinkCache::singleton();
+
+               if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
+                       wfProfileIn( $fname.'-check' );
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $page = $dbr->tableName( 'page' );
+                       $threshold = $wgUser->getOption('stubthreshold');
+
+                       # Sort by namespace
+                       asort( $this->mLinkHolders['namespaces'] );
+
+                       # Generate query
+                       $query = false;
+                       $current = null;
+                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+                               # Make title object
+                               $title = $this->mLinkHolders['titles'][$key];
+
+                               # Skip invalid entries.
+                               # Result will be ugly, but prevents crash.
+                               if ( is_null( $title ) ) {
+                                       continue;
+                               }
+                               $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
+
+                               # Check if it's a static known link, e.g. interwiki
+                               if ( $title->isAlwaysKnown() ) {
+                                       $colours[$pdbk] = '';
+                               } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
+                                       $colours[$pdbk] = '';
+                                       $this->mOutput->addLink( $title, $id );
+                               } elseif ( $linkCache->isBadLink( $pdbk ) ) {
+                                       $colours[$pdbk] = 'new';
+                               } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
+                                       $colours[$pdbk] = 'new';
+                               } else {
+                                       # Not in the link cache, add it to the query
+                                       if ( !isset( $current ) ) {
+                                               $current = $ns;
+                                               $query =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
+                                               $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
+                                       } elseif ( $current != $ns ) {
+                                               $current = $ns;
+                                               $query .= ")) OR (page_namespace=$ns AND page_title IN(";
+                                       } else {
+                                               $query .= ', ';
+                                       }
+
+                                       $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
+                               }
+                       }
+                       if ( $query ) {
+                               $query .= '))';
+                               if ( $options & RLH_FOR_UPDATE ) {
+                                       $query .= ' FOR UPDATE';
+                               }
+
+                               $res = $dbr->query( $query, $fname );
+
+                               # Fetch data and form into an associative array
+                               # non-existent = broken
+                               while ( $s = $dbr->fetchObject($res) ) {
+                                       $title = Title::makeTitle( $s->page_namespace, $s->page_title );
+                                       $pdbk = $title->getPrefixedDBkey();
+                                       $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
+                                       $this->mOutput->addLink( $title, $s->page_id );
+                                       $colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
+                                       //add id to the extension todolist
+                                       $linkcolour_ids[$s->page_id] = $pdbk;
+                               }
+                               //pass an array of page_ids to an extension
+                               wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
+                       }
+                       wfProfileOut( $fname.'-check' );
+
+                       # Do a second query for different language variants of links and categories
+                       if($wgContLang->hasVariants()){
+                               $linkBatch = new LinkBatch();
+                               $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
+                               $categoryMap = array(); // maps $category_variant => $category (dbkeys)
+                               $varCategories = array(); // category replacements oldDBkey => newDBkey
+
+                               $categories = $this->mOutput->getCategoryLinks();
+
+                               // Add variants of links to link batch
+                               foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+                                       $title = $this->mLinkHolders['titles'][$key];
+                                       if ( is_null( $title ) )
+                                               continue;
+
+                                       $pdbk = $title->getPrefixedDBkey();
+                                       $titleText = $title->getText();
+
+                                       // generate all variants of the link title text
+                                       $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
+
+                                       // if link was not found (in first query), add all variants to query
+                                       if ( !isset($colours[$pdbk]) ){
+                                               foreach($allTextVariants as $textVariant){
+                                                       if($textVariant != $titleText){
+                                                               $variantTitle = Title::makeTitle( $ns, $textVariant );
+                                                               if(is_null($variantTitle)) continue;
+                                                               $linkBatch->addObj( $variantTitle );
+                                                               $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               // process categories, check if a category exists in some variant
+                               foreach( $categories as $category ){
+                                       $variants = $wgContLang->convertLinkToAllVariants($category);
+                                       foreach($variants as $variant){
+                                               if($variant != $category){
+                                                       $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
+                                                       if(is_null($variantTitle)) continue;
+                                                       $linkBatch->addObj( $variantTitle );
+                                                       $categoryMap[$variant] = $category;
+                                               }
+                                       }
+                               }
+
+
+                               if(!$linkBatch->isEmpty()){
+                                       // construct query
+                                       $titleClause = $linkBatch->constructSet('page', $dbr);
+
+                                       $variantQuery =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
+
+                                       $variantQuery .= " FROM $page WHERE $titleClause";
+                                       if ( $options & RLH_FOR_UPDATE ) {
+                                               $variantQuery .= ' FOR UPDATE';
+                                       }
+
+                                       $varRes = $dbr->query( $variantQuery, $fname );
+
+                                       // for each found variants, figure out link holders and replace
+                                       while ( $s = $dbr->fetchObject($varRes) ) {
+
+                                               $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
+                                               $varPdbk = $variantTitle->getPrefixedDBkey();
+                                               $vardbk = $variantTitle->getDBkey();
+
+                                               $holderKeys = array();
+                                               if(isset($variantMap[$varPdbk])){
+                                                       $holderKeys = $variantMap[$varPdbk];
+                                                       $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
+                                                       $this->mOutput->addLink( $variantTitle, $s->page_id );
+                                               }
+
+                                               // loop over link holders
+                                               foreach($holderKeys as $key){
+                                                       $title = $this->mLinkHolders['titles'][$key];
+                                                       if ( is_null( $title ) ) continue;
+
+                                                       $pdbk = $title->getPrefixedDBkey();
+
+                                                       if(!isset($colours[$pdbk])){
+                                                               // found link in some of the variants, replace the link holder data
+                                                               $this->mLinkHolders['titles'][$key] = $variantTitle;
+                                                               $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
+
+                                                               // set pdbk and colour
+                                                               $pdbks[$key] = $varPdbk;
+                                                               $colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
+                                                               $linkcolour_ids[$s->page_id] = $pdbk;
+                                                       }
+                                                       wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
+                                               }
+
+                                               // check if the object is a variant of a category
+                                               if(isset($categoryMap[$vardbk])){
+                                                       $oldkey = $categoryMap[$vardbk];
+                                                       if($oldkey != $vardbk)
+                                                               $varCategories[$oldkey]=$vardbk;
+                                               }
+                                       }
+
+                                       // rebuild the categories in original order (if there are replacements)
+                                       if(count($varCategories)>0){
+                                               $newCats = array();
+                                               $originalCats = $this->mOutput->getCategories();
+                                               foreach($originalCats as $cat => $sortkey){
+                                                       // make the replacement
+                                                       if( array_key_exists($cat,$varCategories) )
+                                                               $newCats[$varCategories[$cat]] = $sortkey;
+                                                       else $newCats[$cat] = $sortkey;
+                                               }
+                                               $this->mOutput->setCategoryLinks($newCats);
+                                       }
+                               }
+                       }
+
+                       # Construct search and replace arrays
+                       wfProfileIn( $fname.'-construct' );
+                       $replacePairs = array();
+                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+                               $pdbk = $pdbks[$key];
+                               $searchkey = "<!--LINK $key-->";
+                               $title = $this->mLinkHolders['titles'][$key];
+                               if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
+                                       $linkCache->addBadLinkObj( $title );
+                                       $colours[$pdbk] = 'new';
+                                       $this->mOutput->addLink( $title, 0 );
+                                       $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
+                                                                       $this->mLinkHolders['texts'][$key],
+                                                                       $this->mLinkHolders['queries'][$key] );
+                               } else {
+                                       $replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
+                                                                       $this->mLinkHolders['texts'][$key],
+                                                                       $this->mLinkHolders['queries'][$key] );
+                               }
+                       }
+                       $replacer = new HashtableReplacer( $replacePairs, 1 );
+                       wfProfileOut( $fname.'-construct' );
+
+                       # Do the thing
+                       wfProfileIn( $fname.'-replace' );
+                       $text = preg_replace_callback(
+                               '/(<!--LINK .*?-->)/',
+                               $replacer->cb(),
+                               $text);
+
+                       wfProfileOut( $fname.'-replace' );
+               }
+
+               # Now process interwiki link holders
+               # This is quite a bit simpler than internal links
+               if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
+                       wfProfileIn( $fname.'-interwiki' );
+                       # Make interwiki link HTML
+                       $replacePairs = array();
+                       foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
+                               $title = $this->mInterwikiLinkHolders['titles'][$key];
+                               $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
+                       }
+                       $replacer = new HashtableReplacer( $replacePairs, 1 );
+
+                       $text = preg_replace_callback(
+                               '/<!--IWLINK (.*?)-->/',
+                               $replacer->cb(),
+                               $text );
+                       wfProfileOut( $fname.'-interwiki' );
+               }
+
+               wfProfileOut( $fname );
+               return $colours;
+       }
+
+       /**
+        * Replace <!--LINK--> link placeholders with plain text of links
+        * (not HTML-formatted).
+        * @param string $text
+        * @return string
+        */
+       function replaceLinkHoldersText( $text ) {
+               $fname = 'Parser::replaceLinkHoldersText';
+               wfProfileIn( $fname );
+
+               $text = preg_replace_callback(
+                       '/<!--(LINK|IWLINK) (.*?)-->/',
+                       array( &$this, 'replaceLinkHoldersTextCallback' ),
+                       $text );
+
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * @param array $matches
+        * @return string
+        * @private
+        */
+       function replaceLinkHoldersTextCallback( $matches ) {
+               $type = $matches[1];
+               $key  = $matches[2];
+               if( $type == 'LINK' ) {
+                       if( isset( $this->mLinkHolders['texts'][$key] ) ) {
+                               return $this->mLinkHolders['texts'][$key];
+                       }
+               } elseif( $type == 'IWLINK' ) {
+                       if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
+                               return $this->mInterwikiLinkHolders['texts'][$key];
+                       }
+               }
+               return $matches[0];
+       }
+
+       /**
+        * Tag hook handler for 'pre'.
+        */
+       function renderPreTag( $text, $attribs ) {
+               // Backwards-compatibility hack
+               $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
+
+               $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
+               return wfOpenElement( 'pre', $attribs ) .
+                       Xml::escapeTagsOnly( $content ) .
+                       '</pre>';
+       }
+
+       /**
+        * Renders an image gallery from a text with one line per image.
+        * text labels may be given by using |-style alternative text. E.g.
+        *   Image:one.jpg|The number "1"
+        *   Image:tree.jpg|A tree
+        * given as text will return the HTML of a gallery with two images,
+        * labeled 'The number "1"' and
+        * 'A tree'.
+        */
+       function renderImageGallery( $text, $params ) {
+               $ig = new ImageGallery();
+               $ig->setContextTitle( $this->mTitle );
+               $ig->setShowBytes( false );
+               $ig->setShowFilename( false );
+               $ig->setParser( $this );
+               $ig->setHideBadImages();
+               $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
+               $ig->useSkin( $this->mOptions->getSkin() );
+               $ig->mRevisionId = $this->mRevisionId;
+
+               if( isset( $params['caption'] ) ) {
+                       $caption = $params['caption'];
+                       $caption = htmlspecialchars( $caption );
+                       $caption = $this->replaceInternalLinks( $caption );
+                       $ig->setCaptionHtml( $caption );
+               }
+               if( isset( $params['perrow'] ) ) {
+                       $ig->setPerRow( $params['perrow'] );
+               }
+               if( isset( $params['widths'] ) ) {
+                       $ig->setWidths( $params['widths'] );
+               }
+               if( isset( $params['heights'] ) ) {
+                       $ig->setHeights( $params['heights'] );
+               }
+
+               wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
+
+               $lines = explode( "\n", $text );
+               foreach ( $lines as $line ) {
+                       # match lines like these:
+                       # Image:someimage.jpg|This is some image
+                       $matches = array();
+                       preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
+                       # Skip empty lines
+                       if ( count( $matches ) == 0 ) {
+                               continue;
+                       }
+                       
+                       if ( strpos( $matches[0], '%' ) !== false )
+                               $matches[1] = urldecode( $matches[1] );
+                       $tp = Title::newFromText( $matches[1] );
+                       $nt =& $tp;
+                       if( is_null( $nt ) ) {
+                               # Bogus title. Ignore these so we don't bomb out later.
+                               continue;
+                       }
+                       if ( isset( $matches[3] ) ) {
+                               $label = $matches[3];
+                       } else {
+                               $label = '';
+                       }
+
+                       $html = $this->recursiveTagParse( trim( $label ) );
+
+                       $ig->add( $nt, $html );
+
+                       # Only add real images (bug #5586)
+                       if ( $nt->getNamespace() == NS_IMAGE ) {
+                               $this->mOutput->addImage( $nt->getDBkey() );
+                       }
+               }
+               return $ig->toHTML();
+       }
+
+       function getImageParams( $handler ) {
+               if ( $handler ) {
+                       $handlerClass = get_class( $handler );
+               } else {
+                       $handlerClass = '';
+               }
+               if ( !isset( $this->mImageParams[$handlerClass]  ) ) {
+                       // Initialise static lists
+                       static $internalParamNames = array(
+                               'horizAlign' => array( 'left', 'right', 'center', 'none' ),
+                               'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
+                                       'bottom', 'text-bottom' ),
+                               'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
+                                       'upright', 'border' ),
+                       );
+                       static $internalParamMap;
+                       if ( !$internalParamMap ) {
+                               $internalParamMap = array();
+                               foreach ( $internalParamNames as $type => $names ) {
+                                       foreach ( $names as $name ) {
+                                               $magicName = str_replace( '-', '_', "img_$name" );
+                                               $internalParamMap[$magicName] = array( $type, $name );
+                                       }
+                               }
+                       }
+
+                       // Add handler params
+                       $paramMap = $internalParamMap;
+                       if ( $handler ) {
+                               $handlerParamMap = $handler->getParamMap();
+                               foreach ( $handlerParamMap as $magic => $paramName ) {
+                                       $paramMap[$magic] = array( 'handler', $paramName );
+                               }
+                       }
+                       $this->mImageParams[$handlerClass] = $paramMap;
+                       $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
+               }
+               return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
+       }
+
+       /**
+        * Parse image options text and use it to make an image
+        */
+       function makeImage( $title, $options ) {
+               # Check if the options text is of the form "options|alt text"
+               # Options are:
+               #  * thumbnail          make a thumbnail with enlarge-icon and caption, alignment depends on lang
+               #  * left               no resizing, just left align. label is used for alt= only
+               #  * right              same, but right aligned
+               #  * none               same, but not aligned
+               #  * ___px              scale to ___ pixels width, no aligning. e.g. use in taxobox
+               #  * center             center the image
+               #  * framed             Keep original image size, no magnify-button.
+               #  * frameless          like 'thumb' but without a frame. Keeps user preferences for width
+               #  * upright            reduce width for upright images, rounded to full __0 px
+               #  * border             draw a 1px border around the image
+               # vertical-align values (no % or length right now):
+               #  * baseline
+               #  * sub
+               #  * super
+               #  * top
+               #  * text-top
+               #  * middle
+               #  * bottom
+               #  * text-bottom
+
+               $parts = array_map( 'trim', explode( '|', $options) );
+               $sk = $this->mOptions->getSkin();
+
+               # Give extensions a chance to select the file revision for us
+               $skip = $time = $descQuery = false;
+               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time, &$descQuery ) );
+
+               if ( $skip ) {
+                       return $sk->makeLinkObj( $title );
+               }
+
+               # Get parameter map
+               $file = wfFindFile( $title, $time );
+               $handler = $file ? $file->getHandler() : false;
+
+               list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
+
+               # Process the input parameters
+               $caption = '';
+               $params = array( 'frame' => array(), 'handler' => array(),
+                       'horizAlign' => array(), 'vertAlign' => array() );
+               foreach( $parts as $part ) {
+                       list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
+                       $validated = false;
+                       if( isset( $paramMap[$magicName] ) ) {
+                               list( $type, $paramName ) = $paramMap[$magicName];
+
+                               // Special case; width and height come in one variable together
+                               if( $type == 'handler' && $paramName == 'width' ) {
+                                       $m = array();
+                                       # (bug 13500) In both cases (width/height and width only),
+                                       # permit trailing "px" for backward compatibility.
+                                       if ( preg_match( '/^([0-9]*)x([0-9]*)\s*(?:px)?\s*$/', $value, $m ) ) {
+                                               $width = intval( $m[1] );
+                                               $height = intval( $m[2] );
+                                               if ( $handler->validateParam( 'width', $width ) ) {
+                                                       $params[$type]['width'] = $width;
+                                                       $validated = true;
+                                               }
+                                               if ( $handler->validateParam( 'height', $height ) ) {
+                                                       $params[$type]['height'] = $height;
+                                                       $validated = true;
+                                               }
+                                       } elseif ( preg_match( '/^[0-9]*\s*(?:px)?\s*$/', $value ) ) {
+                                               $width = intval( $value );
+                                               if ( $handler->validateParam( 'width', $width ) ) {
+                                                       $params[$type]['width'] = $width;
+                                                       $validated = true;
+                                               }
+                                       } // else no validation -- bug 13436
+                               } else {
+                                       if ( $type == 'handler' ) {
+                                               # Validate handler parameter
+                                               $validated = $handler->validateParam( $paramName, $value );
+                                       } else {
+                                               # Validate internal parameters
+                                               switch( $paramName ) {
+                                               case "manualthumb":
+                                                       /// @fixme - possibly check validity here?
+                                                       /// downstream behavior seems odd with missing manual thumbs.
+                                                       $validated = true;
+                                                       break;
+                                               default:
+                                                       // Most other things appear to be empty or numeric...
+                                                       $validated = ( $value === false || is_numeric( trim( $value ) ) );
+                                               }
+                                       }
+
+                                       if ( $validated ) {
+                                               $params[$type][$paramName] = $value;
+                                       }
+                               }
+                       }
+                       if ( !$validated ) {
+                               $caption = $part;
+                       }
+               }
+
+               # Process alignment parameters
+               if ( $params['horizAlign'] ) {
+                       $params['frame']['align'] = key( $params['horizAlign'] );
+               }
+               if ( $params['vertAlign'] ) {
+                       $params['frame']['valign'] = key( $params['vertAlign'] );
+               }
+
+               # Strip bad stuff out of the alt text
+               $alt = $this->replaceLinkHoldersText( $caption );
+
+               # make sure there are no placeholders in thumbnail attributes
+               # that are later expanded to html- so expand them now and
+               # remove the tags
+               $alt = $this->mStripState->unstripBoth( $alt );
+               $alt = Sanitizer::stripAllTags( $alt );
+
+               $params['frame']['alt'] = $alt;
+               $params['frame']['caption'] = $caption;
+
+               wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
+
+               # Linker does the rest
+               $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery );
+
+               # Give the handler a chance to modify the parser object
+               if ( $handler ) {
+                       $handler->parserTransformHook( $this, $file );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Set a flag in the output object indicating that the content is dynamic and
+        * shouldn't be cached.
+        */
+       function disableCache() {
+               wfDebug( "Parser output marked as uncacheable.\n" );
+               $this->mOutput->mCacheTime = -1;
+       }
+
+       /**#@+
+        * Callback from the Sanitizer for expanding items found in HTML attribute
+        * values, so they can be safely tested and escaped.
+        * @param string $text
+        * @param PPFrame $frame
+        * @return string
+        * @private
+        */
+       function attributeStripCallback( &$text, $frame = false ) {
+               $text = $this->replaceVariables( $text, $frame );
+               $text = $this->mStripState->unstripBoth( $text );
+               return $text;
+       }
+
+       /**#@-*/
+
+       /**#@+
+        * Accessor/mutator
+        */
+       function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
+       function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
+       function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
+       /**#@-*/
+
+       /**#@+
+        * Accessor
+        */
+       function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
+       /**#@-*/
+
+
+       /**
+        * Break wikitext input into sections, and either pull or replace
+        * some particular section's text.
+        *
+        * External callers should use the getSection and replaceSection methods.
+        *
+        * @param string $text Page wikitext
+        * @param string $section A section identifier string of the form:
+        *   <flag1> - <flag2> - ... - <section number>
+        *
+        * Currently the only recognised flag is "T", which means the target section number
+        * was derived during a template inclusion parse, in other words this is a template
+        * section edit link. If no flags are given, it was an ordinary section edit link.
+        * This flag is required to avoid a section numbering mismatch when a section is
+        * enclosed by <includeonly> (bug 6563).
+        *
+        * The section number 0 pulls the text before the first heading; other numbers will
+        * pull the given section along with its lower-level subsections. If the section is
+        * not found, $mode=get will return $newtext, and $mode=replace will return $text.
+        *
+        * @param string $mode One of "get" or "replace"
+        * @param string $newText Replacement text for section data.
+        * @return string for "get", the extracted section text.
+        *                for "replace", the whole page with the section replaced.
+        */
+       private function extractSections( $text, $section, $mode, $newText='' ) {
+               global $wgTitle;
+               $this->clearState();
+               $this->setTitle( $wgTitle ); // not generally used but removes an ugly failure mode
+               $this->mOptions = new ParserOptions;
+               $this->setOutputType( self::OT_WIKI );
+               $outText = '';
+               $frame = $this->getPreprocessor()->newFrame();
+
+               // Process section extraction flags
+               $flags = 0;
+               $sectionParts = explode( '-', $section );
+               $sectionIndex = array_pop( $sectionParts );
+               foreach ( $sectionParts as $part ) {
+                       if ( $part == 'T' ) {
+                               $flags |= self::PTD_FOR_INCLUSION;
+                       }
+               }
+               // Preprocess the text
+               $root = $this->preprocessToDom( $text, $flags );
+
+               // <h> nodes indicate section breaks
+               // They can only occur at the top level, so we can find them by iterating the root's children
+               $node = $root->getFirstChild();
+
+               // Find the target section
+               if ( $sectionIndex == 0 ) {
+                       // Section zero doesn't nest, level=big
+                       $targetLevel = 1000;
+               } else {
+            while ( $node ) {
+                if ( $node->getName() == 'h' ) {
+                    $bits = $node->splitHeading();
+                                       if ( $bits['i'] == $sectionIndex ) {
+                                       $targetLevel = $bits['level'];
+                                               break;
+                                       }
+                               }
+                               if ( $mode == 'replace' ) {
+                                       $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
+                               }
+                               $node = $node->getNextSibling();
+                       }
+               }
+
+               if ( !$node ) {
+                       // Not found
+                       if ( $mode == 'get' ) {
+                               return $newText;
+                       } else {
+                               return $text;
+                       }
+               }
+
+               // Find the end of the section, including nested sections
+               do {
+                       if ( $node->getName() == 'h' ) {
+                               $bits = $node->splitHeading();
+                               $curLevel = $bits['level'];
+                               if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
+                                       break;
+                               }
+                       }
+                       if ( $mode == 'get' ) {
+                               $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
+                       }
+                       $node = $node->getNextSibling();
+               } while ( $node );
+
+               // Write out the remainder (in replace mode only)
+               if ( $mode == 'replace' ) {
+                       // Output the replacement text
+                       // Add two newlines on -- trailing whitespace in $newText is conventionally
+                       // stripped by the editor, so we need both newlines to restore the paragraph gap
+                       $outText .= $newText . "\n\n";
+                       while ( $node ) {
+                               $outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
+                               $node = $node->getNextSibling();
+                       }
+               }
+
+               if ( is_string( $outText ) ) {
+                       // Re-insert stripped tags
+                       $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
+               }
+
+               return $outText;
+       }
+
+       /**
+        * This function returns the text of a section, specified by a number ($section).
+        * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
+        * the first section before any such heading (section 0).
+        *
+        * If a section contains subsections, these are also returned.
+        *
+        * @param string $text text to look in
+        * @param string $section section identifier
+        * @param string $deftext default to return if section is not found
+        * @return string text of the requested section
+        */
+       public function getSection( $text, $section, $deftext='' ) {
+               return $this->extractSections( $text, $section, "get", $deftext );
+       }
+
+       public function replaceSection( $oldtext, $section, $text ) {
+               return $this->extractSections( $oldtext, $section, "replace", $text );
+       }
+
+       /**
+        * Get the timestamp associated with the current revision, adjusted for
+        * the default server-local timestamp
+        */
+       function getRevisionTimestamp() {
+               if ( is_null( $this->mRevisionTimestamp ) ) {
+                       wfProfileIn( __METHOD__ );
+                       global $wgContLang;
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
+                                       array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
+
+                       // Normalize timestamp to internal MW format for timezone processing.
+                       // This has the added side-effect of replacing a null value with
+                       // the current time, which gives us more sensible behavior for
+                       // previews.
+                       $timestamp = wfTimestamp( TS_MW, $timestamp );
+
+                       // The cryptic '' timezone parameter tells to use the site-default
+                       // timezone offset instead of the user settings.
+                       //
+                       // Since this value will be saved into the parser cache, served
+                       // to other users, and potentially even used inside links and such,
+                       // it needs to be consistent for all visitors.
+                       $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
+
+                       wfProfileOut( __METHOD__ );
+               }
+               return $this->mRevisionTimestamp;
+       }
+
+       /**
+        * Mutator for $mDefaultSort
+        *
+        * @param $sort New value
+        */
+       public function setDefaultSort( $sort ) {
+               $this->mDefaultSort = $sort;
+       }
+
+       /**
+        * Accessor for $mDefaultSort
+        * Will use the title/prefixed title if none is set
+        *
+        * @return string
+        */
+       public function getDefaultSort() {
+               if( $this->mDefaultSort !== false ) {
+                       return $this->mDefaultSort;
+               } else {
+                       return $this->mTitle->getNamespace() == NS_CATEGORY
+                                       ? $this->mTitle->getText()
+                                       : $this->mTitle->getPrefixedText();
+               }
+       }
+
+       /**
+        * Try to guess the section anchor name based on a wikitext fragment
+        * presumably extracted from a heading, for example "Header" from
+        * "== Header ==".
+        */
+       public function guessSectionNameFromWikiText( $text ) {
+               # Strip out wikitext links(they break the anchor)
+               $text = $this->stripSectionName( $text );
+               $headline = Sanitizer::decodeCharReferences( $text );
+               # strip out HTML
+               $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
+               $headline = trim( $headline );
+               $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
+               $replacearray = array(
+                       '%3A' => ':',
+                       '%' => '.'
+               );
+               return str_replace(
+                       array_keys( $replacearray ),
+                       array_values( $replacearray ),
+                       $sectionanchor );
+       }
+
+       /**
+        * Strips a text string of wikitext for use in a section anchor
+        *
+        * Accepts a text string and then removes all wikitext from the
+        * string and leaves only the resultant text (i.e. the result of
+        * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
+        * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
+        * to create valid section anchors by mimicing the output of the
+        * parser when headings are parsed.
+        *
+        * @param $text string Text string to be stripped of wikitext
+        * for use in a Section anchor
+        * @return Filtered text string
+        */
+       public function stripSectionName( $text ) {
+               # Strip internal link markup
+               $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
+               $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
+
+               # Strip external link markup (FIXME: Not Tolerant to blank link text
+               # I.E. [http://www.mediawiki.org] will render as [1] or something depending
+               # on how many empty links there are on the page - need to figure that out.
+               $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
+
+               # Parse wikitext quotes (italics & bold)
+               $text = $this->doQuotes($text);
+
+               # Strip HTML tags
+               $text = StringUtils::delimiterReplace( '<', '>', '', $text );
+               return $text;
+       }
+
+       function srvus( $text ) {
+               return $this->testSrvus( $text, $this->mOutputType );
+       }
+
+       /**
+        * strip/replaceVariables/unstrip for preprocessor regression testing
+        */
+       function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
+               $this->clearState();
+               if ( ! ( $title instanceof Title ) ) {
+                       $title = Title::newFromText( $title );
+               }
+               $this->mTitle = $title;
+               $this->mOptions = $options;
+               $this->setOutputType( $outputType );
+               $text = $this->replaceVariables( $text );
+               $text = $this->mStripState->unstripBoth( $text );
+               $text = Sanitizer::removeHTMLtags( $text );
+               return $text;
+       }
+
+       function testPst( $text, $title, $options ) {
+               global $wgUser;
+               if ( ! ( $title instanceof Title ) ) {
+                       $title = Title::newFromText( $title );
+               }
+               return $this->preSaveTransform( $text, $title, $wgUser, $options );
+       }
+
+       function testPreprocess( $text, $title, $options ) {
+               if ( ! ( $title instanceof Title ) ) {
+                       $title = Title::newFromText( $title );
+               }
+               return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
+       }
+
+       function markerSkipCallback( $s, $callback ) {
+               $i = 0;
+               $out = '';
+               while ( $i < strlen( $s ) ) {
+                       $markerStart = strpos( $s, $this->mUniqPrefix, $i );
+                       if ( $markerStart === false ) {
+                               $out .= call_user_func( $callback, substr( $s, $i ) );
+                               break;
+                       } else {
+                               $out .= call_user_func( $callback, substr( $s, $i, $markerStart - $i ) );
+                               $markerEnd = strpos( $s, self::MARKER_SUFFIX, $markerStart );
+                               if ( $markerEnd === false ) {
+                                       $out .= substr( $s, $markerStart );
+                                       break;
+                               } else {
+                                       $markerEnd += strlen( self::MARKER_SUFFIX );
+                                       $out .= substr( $s, $markerStart, $markerEnd - $markerStart );
+                                       $i = $markerEnd;
+                               }
+                       }
+               }
+               return $out;
+       }
+}
+
+/**
+ * @todo document, briefly.
+ * @ingroup Parser
+ */
+class StripState {
+       var $general, $nowiki;
+
+       function __construct() {
+               $this->general = new ReplacementArray;
+               $this->nowiki = new ReplacementArray;
+       }
+
+       function unstripGeneral( $text ) {
+               wfProfileIn( __METHOD__ );
+               do {
+                       $oldText = $text;
+                       $text = $this->general->replace( $text );
+               } while ( $text != $oldText );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       function unstripNoWiki( $text ) {
+               wfProfileIn( __METHOD__ );
+               do {
+                       $oldText = $text;
+                       $text = $this->nowiki->replace( $text );
+               } while ( $text != $oldText );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       function unstripBoth( $text ) {
+               wfProfileIn( __METHOD__ );
+               do {
+                       $oldText = $text;
+                       $text = $this->general->replace( $text );
+                       $text = $this->nowiki->replace( $text );
+               } while ( $text != $oldText );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+}
+
+/**
+ * @todo document, briefly.
+ * @ingroup Parser
+ */
+class OnlyIncludeReplacer {
+       var $output = '';
+
+       function replace( $matches ) {
+               if ( substr( $matches[1], -1 ) == "\n" ) {
+                       $this->output .= substr( $matches[1], 0, -1 );
+               } else {
+                       $this->output .= $matches[1];
+               }
+       }
+}
diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php
new file mode 100644 (file)
index 0000000..bf11da2
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * @ingroup Cache Parser
+ * @todo document
+ */
+class ParserCache {
+       /**
+        * Get an instance of this object
+        */
+       public static function &singleton() {
+               static $instance;
+               if ( !isset( $instance ) ) {
+                       global $parserMemc;
+                       $instance = new ParserCache( $parserMemc );
+               }
+               return $instance;
+       }
+
+       /**
+        * Setup a cache pathway with a given back-end storage mechanism.
+        * May be a memcached client or a BagOStuff derivative.
+        *
+        * @param object $memCached
+        */
+       function __construct( &$memCached ) {
+               $this->mMemc =& $memCached;
+       }
+
+       function getKey( &$article, &$user ) {
+               global $action;
+               $hash = $user->getPageRenderingHash();
+               if( !$article->mTitle->quickUserCan( 'edit' ) ) {
+                       // section edit links are suppressed even if the user has them on
+                       $edit = '!edit=0';
+               } else {
+                       $edit = '';
+               }
+               $pageid = intval( $article->getID() );
+               $renderkey = (int)($action == 'render');
+               $key = wfMemcKey( 'pcache', 'idhash', "$pageid-$renderkey!$hash$edit" );
+               return $key;
+       }
+
+       function getETag( &$article, &$user ) {
+               return 'W/"' . $this->getKey($article, $user) . "--" . $article->mTouched. '"';
+       }
+
+       function get( &$article, &$user ) {
+               global $wgCacheEpoch;
+               $fname = 'ParserCache::get';
+               wfProfileIn( $fname );
+
+               $key = $this->getKey( $article, $user );
+
+               wfDebug( "Trying parser cache $key\n" );
+               $value = $this->mMemc->get( $key );
+               if ( is_object( $value ) ) {
+                       wfDebug( "Found.\n" );
+                       # Delete if article has changed since the cache was made
+                       $canCache = $article->checkTouched();
+                       $cacheTime = $value->getCacheTime();
+                       $touched = $article->mTouched;
+                       if ( !$canCache || $value->expired( $touched ) ) {
+                               if ( !$canCache ) {
+                                       wfIncrStats( "pcache_miss_invalid" );
+                                       wfDebug( "Invalid cached redirect, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
+                               } else {
+                                       wfIncrStats( "pcache_miss_expired" );
+                                       wfDebug( "Key expired, touched $touched, epoch $wgCacheEpoch, cached $cacheTime\n" );
+                               }
+                               $this->mMemc->delete( $key );
+                               $value = false;
+                       } else {
+                               if ( isset( $value->mTimestamp ) ) {
+                                       $article->mTimestamp = $value->mTimestamp;
+                               }
+                               wfIncrStats( "pcache_hit" );
+                       }
+               } else {
+                       wfDebug( "Parser cache miss.\n" );
+                       wfIncrStats( "pcache_miss_absent" );
+                       $value = false;
+               }
+
+               wfProfileOut( $fname );
+               return $value;
+       }
+
+       function save( $parserOutput, &$article, &$user ){
+               global $wgParserCacheExpireTime;
+               $key = $this->getKey( $article, $user );
+
+               if( $parserOutput->getCacheTime() != -1 ) {
+
+                       $now = wfTimestampNow();
+                       $parserOutput->setCacheTime( $now );
+
+                       // Save the timestamp so that we don't have to load the revision row on view
+                       $parserOutput->mTimestamp = $article->getTimestamp();
+
+                       $parserOutput->mText .= "\n<!-- Saved in parser cache with key $key and timestamp $now -->\n";
+                       wfDebug( "Saved in parser cache with key $key and timestamp $now\n" );
+
+                       if( $parserOutput->containsOldMagic() ){
+                               $expire = 3600; # 1 hour
+                       } else {
+                               $expire = $wgParserCacheExpireTime;
+                       }
+                       $this->mMemc->set( $key, $parserOutput, $expire );
+
+               } else {
+                       wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" );
+               }
+       }
+
+}
diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php
new file mode 100644 (file)
index 0000000..330ec44
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * Set options of the Parser
+ * @todo document
+ * @ingroup Parser
+ */
+class ParserOptions
+{
+       # All variables are supposed to be private in theory, although in practise this is not the case.
+       var $mUseTeX;                    # Use texvc to expand <math> tags
+       var $mUseDynamicDates;           # Use DateFormatter to format dates
+       var $mInterwikiMagic;            # Interlanguage links are removed and returned in an array
+       var $mAllowExternalImages;       # Allow external images inline
+       var $mAllowExternalImagesFrom;   # If not, any exception?
+       var $mSkin;                      # Reference to the preferred skin
+       var $mDateFormat;                # Date format index
+       var $mEditSection;               # Create "edit section" links
+       var $mNumberHeadings;            # Automatically number headings
+       var $mAllowSpecialInclusion;     # Allow inclusion of special pages
+       var $mTidy;                      # Ask for tidy cleanup
+       var $mInterfaceMessage;          # Which lang to call for PLURAL and GRAMMAR
+       var $mTargetLanguage;            # Overrides above setting with arbitrary language
+       var $mMaxIncludeSize;            # Maximum size of template expansions, in bytes
+       var $mMaxPPNodeCount;            # Maximum number of nodes touched by PPFrame::expand()
+       var $mMaxPPExpandDepth;          # Maximum recursion depth in PPFrame::expand()
+       var $mMaxTemplateDepth;          # Maximum recursion depth for templates within templates
+       var $mRemoveComments;            # Remove HTML comments. ONLY APPLIES TO PREPROCESS OPERATIONS
+       var $mTemplateCallback;          # Callback for template fetching
+       var $mEnableLimitReport;         # Enable limit report in an HTML comment on output
+       var $mTimestamp;                 # Timestamp used for {{CURRENTDAY}} etc.
+
+       var $mUser;                      # Stored user object, just used to initialise the skin
+
+       function getUseTeX()                        { return $this->mUseTeX; }
+       function getUseDynamicDates()               { return $this->mUseDynamicDates; }
+       function getInterwikiMagic()                { return $this->mInterwikiMagic; }
+       function getAllowExternalImages()           { return $this->mAllowExternalImages; }
+       function getAllowExternalImagesFrom()       { return $this->mAllowExternalImagesFrom; }
+       function getEditSection()                   { return $this->mEditSection; }
+       function getNumberHeadings()                { return $this->mNumberHeadings; }
+       function getAllowSpecialInclusion()         { return $this->mAllowSpecialInclusion; }
+       function getTidy()                          { return $this->mTidy; }
+       function getInterfaceMessage()              { return $this->mInterfaceMessage; }
+       function getTargetLanguage()                { return $this->mTargetLanguage; }
+       function getMaxIncludeSize()                { return $this->mMaxIncludeSize; }
+       function getMaxPPNodeCount()                { return $this->mMaxPPNodeCount; }
+       function getMaxTemplateDepth()              { return $this->mMaxTemplateDepth; }
+       function getRemoveComments()                { return $this->mRemoveComments; }
+       function getTemplateCallback()              { return $this->mTemplateCallback; }
+       function getEnableLimitReport()             { return $this->mEnableLimitReport; }
+
+       function getSkin() {
+               if ( !isset( $this->mSkin ) ) {
+                       $this->mSkin = $this->mUser->getSkin();
+               }
+               return $this->mSkin;
+       }
+
+       function getDateFormat() {
+               if ( !isset( $this->mDateFormat ) ) {
+                       $this->mDateFormat = $this->mUser->getDatePreference();
+               }
+               return $this->mDateFormat;
+       }
+
+       function getTimestamp() {
+               if ( !isset( $this->mTimestamp ) ) {
+                       $this->mTimestamp = wfTimestampNow();
+               }
+               return $this->mTimestamp;
+       }
+
+       function setUseTeX( $x )                    { return wfSetVar( $this->mUseTeX, $x ); }
+       function setUseDynamicDates( $x )           { return wfSetVar( $this->mUseDynamicDates, $x ); }
+       function setInterwikiMagic( $x )            { return wfSetVar( $this->mInterwikiMagic, $x ); }
+       function setAllowExternalImages( $x )       { return wfSetVar( $this->mAllowExternalImages, $x ); }
+       function setAllowExternalImagesFrom( $x )   { return wfSetVar( $this->mAllowExternalImagesFrom, $x ); }
+       function setDateFormat( $x )                { return wfSetVar( $this->mDateFormat, $x ); }
+       function setEditSection( $x )               { return wfSetVar( $this->mEditSection, $x ); }
+       function setNumberHeadings( $x )            { return wfSetVar( $this->mNumberHeadings, $x ); }
+       function setAllowSpecialInclusion( $x )     { return wfSetVar( $this->mAllowSpecialInclusion, $x ); }
+       function setTidy( $x )                      { return wfSetVar( $this->mTidy, $x); }
+       function setSkin( $x )                      { $this->mSkin = $x; }
+       function setInterfaceMessage( $x )          { return wfSetVar( $this->mInterfaceMessage, $x); }
+       function setTargetLanguage( $x )            { return wfSetVar( $this->mTargetLanguage, $x); }
+       function setMaxIncludeSize( $x )            { return wfSetVar( $this->mMaxIncludeSize, $x ); }
+       function setMaxPPNodeCount( $x )            { return wfSetVar( $this->mMaxPPNodeCount, $x ); }
+       function setMaxTemplateDepth( $x )          { return wfSetVar( $this->mMaxTemplateDepth, $x ); }
+       function setRemoveComments( $x )            { return wfSetVar( $this->mRemoveComments, $x ); }
+       function setTemplateCallback( $x )          { return wfSetVar( $this->mTemplateCallback, $x ); }
+       function enableLimitReport( $x = true )     { return wfSetVar( $this->mEnableLimitReport, $x ); }
+       function setTimestamp( $x )                 { return wfSetVar( $this->mTimestamp, $x ); }
+
+       function __construct( $user = null ) {
+               $this->initialiseFromUser( $user );
+       }
+
+       /**
+        * Get parser options
+        * @static
+        */
+       static function newFromUser( $user ) {
+               return new ParserOptions( $user );
+       }
+
+       /** Get user options */
+       function initialiseFromUser( $userInput ) {
+               global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
+               global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize;
+               global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth;
+               $fname = 'ParserOptions::initialiseFromUser';
+               wfProfileIn( $fname );
+               if ( !$userInput ) {
+                       global $wgUser;
+                       if ( isset( $wgUser ) ) {
+                               $user = $wgUser;
+                       } else {
+                               $user = new User;
+                       }
+               } else {
+                       $user =& $userInput;
+               }
+
+               $this->mUser = $user;
+
+               $this->mUseTeX = $wgUseTeX;
+               $this->mUseDynamicDates = $wgUseDynamicDates;
+               $this->mInterwikiMagic = $wgInterwikiMagic;
+               $this->mAllowExternalImages = $wgAllowExternalImages;
+               $this->mAllowExternalImagesFrom = $wgAllowExternalImagesFrom;
+               $this->mSkin = null; # Deferred
+               $this->mDateFormat = null; # Deferred
+               $this->mEditSection = true;
+               $this->mNumberHeadings = $user->getOption( 'numberheadings' );
+               $this->mAllowSpecialInclusion = $wgAllowSpecialInclusion;
+               $this->mTidy = false;
+               $this->mInterfaceMessage = false;
+               $this->mTargetLanguage = null; // default depends on InterfaceMessage setting
+               $this->mMaxIncludeSize = $wgMaxArticleSize * 1024;
+               $this->mMaxPPNodeCount = $wgMaxPPNodeCount;
+               $this->mMaxPPExpandDepth = $wgMaxPPExpandDepth;
+               $this->mMaxTemplateDepth = $wgMaxTemplateDepth;
+               $this->mRemoveComments = true;
+               $this->mTemplateCallback = array( 'Parser', 'statelessFetchTemplate' );
+               $this->mEnableLimitReport = false;
+               wfProfileOut( $fname );
+       }
+}
diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php
new file mode 100644 (file)
index 0000000..f98d564
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+/**
+ * @todo document
+ * @ingroup Parser
+ */
+class ParserOutput
+{
+       var $mText,             # The output text
+               $mLanguageLinks,    # List of the full text of language links, in the order they appear
+               $mCategories,       # Map of category names to sort keys
+               $mContainsOldMagic, # Boolean variable indicating if the input contained variables like {{CURRENTDAY}}
+               $mCacheTime,        # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
+               $mVersion,          # Compatibility check
+               $mTitleText,        # title text of the chosen language variant
+               $mLinks,            # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken.
+               $mTemplates,        # 2-D map of NS/DBK to ID for the template references. ID=zero for broken.
+               $mTemplateIds,      # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken.
+               $mImages,           # DB keys of the images used, in the array key only
+               $mExternalLinks,    # External link URLs, in the key only
+               $mNewSection,       # Show a new section link?
+               $mNoGallery,        # No gallery on category page? (__NOGALLERY__)
+               $mHeadItems,        # Items to put in the <head> section
+               $mOutputHooks,      # Hook tags as per $wgParserOutputHooks
+               $mWarnings,         # Warning text to be returned to the user. Wikitext formatted, in the key only
+               $mSections,         # Table of contents
+               $mProperties;       # Name/value pairs to be cached in the DB
+
+       /**
+        * Overridden title for display
+        */
+       private $displayTitle = false;
+
+       function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
+               $containsOldMagic = false, $titletext = '' )
+       {
+               $this->mText = $text;
+               $this->mLanguageLinks = $languageLinks;
+               $this->mCategories = $categoryLinks;
+               $this->mContainsOldMagic = $containsOldMagic;
+               $this->mCacheTime = '';
+               $this->mVersion = Parser::VERSION;
+               $this->mTitleText = $titletext;
+               $this->mSections = array();
+               $this->mLinks = array();
+               $this->mTemplates = array();
+               $this->mImages = array();
+               $this->mExternalLinks = array();
+               $this->mNewSection = false;
+               $this->mNoGallery = false;
+               $this->mHeadItems = array();
+               $this->mTemplateIds = array();
+               $this->mOutputHooks = array();
+               $this->mWarnings = array();
+               $this->mProperties = array();
+       }
+
+       function getText()                   { return $this->mText; }
+       function &getLanguageLinks()         { return $this->mLanguageLinks; }
+       function getCategoryLinks()          { return array_keys( $this->mCategories ); }
+       function &getCategories()            { return $this->mCategories; }
+       function getCacheTime()              { return $this->mCacheTime; }
+       function getTitleText()              { return $this->mTitleText; }
+       function getSections()               { return $this->mSections; }
+       function &getLinks()                 { return $this->mLinks; }
+       function &getTemplates()             { return $this->mTemplates; }
+       function &getImages()                { return $this->mImages; }
+       function &getExternalLinks()         { return $this->mExternalLinks; }
+       function getNoGallery()              { return $this->mNoGallery; }
+       function getSubtitle()               { return $this->mSubtitle; }
+       function getOutputHooks()            { return (array)$this->mOutputHooks; }
+       function getWarnings()               { return array_keys( $this->mWarnings ); }
+
+       function containsOldMagic()          { return $this->mContainsOldMagic; }
+       function setText( $text )            { return wfSetVar( $this->mText, $text ); }
+       function setLanguageLinks( $ll )     { return wfSetVar( $this->mLanguageLinks, $ll ); }
+       function setCategoryLinks( $cl )     { return wfSetVar( $this->mCategories, $cl ); }
+       function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
+       function setCacheTime( $t )          { return wfSetVar( $this->mCacheTime, $t ); }
+       function setTitleText( $t )          { return wfSetVar( $this->mTitleText, $t ); }
+       function setSections( $toc )         { return wfSetVar( $this->mSections, $toc ); }
+
+       function addCategory( $c, $sort )    { $this->mCategories[$c] = $sort; }
+       function addLanguageLink( $t )       { $this->mLanguageLinks[] = $t; }
+       function addExternalLink( $url )     { $this->mExternalLinks[$url] = 1; }
+       function addWarning( $s )            { $this->mWarnings[$s] = 1; }
+
+       function addOutputHook( $hook, $data = false ) {
+               $this->mOutputHooks[] = array( $hook, $data );
+       }
+
+       function setNewSection( $value ) {
+               $this->mNewSection = (bool)$value;
+       }
+       function getNewSection() {
+               return (bool)$this->mNewSection;
+       }
+
+       function addLink( $title, $id = null ) {
+               $ns = $title->getNamespace();
+               $dbk = $title->getDBkey();
+               if ( !isset( $this->mLinks[$ns] ) ) {
+                       $this->mLinks[$ns] = array();
+               }
+               if ( is_null( $id ) ) {
+                       $id = $title->getArticleID();
+               }
+               $this->mLinks[$ns][$dbk] = $id;
+       }
+
+       function addImage( $name ) {
+               $this->mImages[$name] = 1;
+       }
+
+       function addTemplate( $title, $page_id, $rev_id ) {
+               $ns = $title->getNamespace();
+               $dbk = $title->getDBkey();
+               if ( !isset( $this->mTemplates[$ns] ) ) {
+                       $this->mTemplates[$ns] = array();
+               }
+               $this->mTemplates[$ns][$dbk] = $page_id;
+               if ( !isset( $this->mTemplateIds[$ns] ) ) {
+                       $this->mTemplateIds[$ns] = array();
+               }
+               $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning
+       }
+
+       /**
+        * Return true if this cached output object predates the global or
+        * per-article cache invalidation timestamps, or if it comes from
+        * an incompatible older version.
+        *
+        * @param string $touched the affected article's last touched timestamp
+        * @return bool
+        * @public
+        */
+       function expired( $touched ) {
+               global $wgCacheEpoch;
+               return $this->getCacheTime() == -1 || // parser says it's uncacheable
+                      $this->getCacheTime() < $touched ||
+                      $this->getCacheTime() <= $wgCacheEpoch ||
+                      !isset( $this->mVersion ) ||
+                      version_compare( $this->mVersion, Parser::VERSION, "lt" );
+       }
+
+       /**
+        * Add some text to the <head>.
+        * If $tag is set, the section with that tag will only be included once
+        * in a given page.
+        */
+       function addHeadItem( $section, $tag = false ) {
+               if ( $tag !== false ) {
+                       $this->mHeadItems[$tag] = $section;
+               } else {
+                       $this->mHeadItems[] = $section;
+               }
+       }
+
+       /**
+        * Override the title to be used for display
+        * -- this is assumed to have been validated
+        * (check equal normalisation, etc.)
+        *
+        * @param string $text Desired title text
+        */
+       public function setDisplayTitle( $text ) {
+               $this->displayTitle = $text;
+       }
+
+       /**
+        * Get the title to be used for display
+        *
+        * @return string
+        */
+       public function getDisplayTitle() {
+               return $this->displayTitle;
+       }
+
+       /**
+        * Fairly generic flag setter thingy.
+        */
+       public function setFlag( $flag ) {
+               $this->mFlags[$flag] = true;
+       }
+
+       public function getFlag( $flag ) {
+               return isset( $this->mFlags[$flag] );
+       }
+
+       /**
+        * Set a property to be cached in the DB
+        */
+       public function setProperty( $name, $value ) {
+               $this->mProperties[$name] = $value;
+       }
+
+       public function getProperty( $name ){
+               return isset( $this->mProperties[$name] ) ? $this->mProperties[$name] : false;
+       }
+
+       public function getProperties() {
+               if ( !isset( $this->mProperties ) ) {
+                       $this->mProperties = array();
+               }
+               return $this->mProperties;
+       }
+}
diff --git a/includes/parser/Parser_DiffTest.php b/includes/parser/Parser_DiffTest.php
new file mode 100644 (file)
index 0000000..be3702c
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @ingroup Parser
+ */
+class Parser_DiffTest
+{
+       var $parsers, $conf;
+
+       var $dfUniqPrefix;
+
+       function __construct( $conf ) {
+               if ( !isset( $conf['parsers'] ) ) {
+                       throw new MWException( __METHOD__ . ': no parsers specified' );
+               }
+               $this->conf = $conf;
+               $this->dtUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+       }
+
+       function init() {
+               if ( !is_null( $this->parsers ) ) {
+                       return;
+               }
+
+               global $wgHooks;
+               static $doneHook = false;
+               if ( !$doneHook ) {
+                       $doneHook = true;
+                       $wgHooks['ParserClearState'][] = array( $this, 'onClearState' );
+               }
+
+               foreach ( $this->conf['parsers'] as $i => $parserConf ) {
+                       if ( !is_array( $parserConf ) ) {
+                               $class = $parserConf;
+                               $parserConf = array( 'class' => $parserConf );
+                       } else {
+                               $class = $parserConf['class'];
+                       }
+                       $this->parsers[$i] = new $class( $parserConf );
+               }
+       }
+
+       function __call( $name, $args ) {
+               $this->init();
+               $results = array();
+               $mismatch = false;
+               $lastResult = null;
+               $first = true;
+               foreach ( $this->parsers as $i => $parser ) {
+                       $currentResult = call_user_func_array( array( &$this->parsers[$i], $name ), $args );
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               if ( is_object( $lastResult ) ) {
+                                       if ( $lastResult != $currentResult ) {
+                                               $mismatch = true;
+                                       }
+                               } else {
+                                       if ( $lastResult !== $currentResult ) {
+                                               $mismatch = true;
+                                       }
+                               }
+                       }
+                       $results[$i] = $currentResult;
+                       $lastResult = $currentResult;
+               }
+               if ( $mismatch ) {
+                       throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" .
+                               'Arguments: ' . var_export( $args, true ) . "\n" .
+                               'Results: ' . var_export( $results, true ) . "\n" );
+               }
+               return $lastResult;
+       }
+
+       function setFunctionHook( $id, $callback, $flags = 0 ) {
+               $this->init();
+               foreach  ( $this->parsers as $i => $parser ) {
+                       $parser->setFunctionHook( $id, $callback, $flags );
+               }
+       }
+
+       function onClearState( &$parser ) {
+               // hack marker prefixes to get identical output
+               $parser->mUniqPrefix = $this->dtUniqPrefix;
+               return true;
+       }
+}
diff --git a/includes/parser/Parser_OldPP.php b/includes/parser/Parser_OldPP.php
new file mode 100644 (file)
index 0000000..0d0394a
--- /dev/null
@@ -0,0 +1,4944 @@
+<?php
+/**
+ * Parser with old preprocessor
+ * @ingroup Parser
+ */
+class Parser_OldPP
+{
+       /**
+        * Update this version number when the ParserOutput format
+        * changes in an incompatible way, so the parser cache
+        * can automatically discard old data.
+        */
+       const VERSION = '1.6.4';
+
+       # Flags for Parser::setFunctionHook
+       # Also available as global constants from Defines.php
+       const SFH_NO_HASH = 1;
+
+       # Constants needed for external link processing
+       # Everything except bracket, space, or control characters
+       const EXT_LINK_URL_CLASS = '[^][<>"\\x00-\\x20\\x7F]';
+       const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)\\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/S';
+
+       // State constants for the definition list colon extraction
+       const COLON_STATE_TEXT = 0;
+       const COLON_STATE_TAG = 1;
+       const COLON_STATE_TAGSTART = 2;
+       const COLON_STATE_CLOSETAG = 3;
+       const COLON_STATE_TAGSLASH = 4;
+       const COLON_STATE_COMMENT = 5;
+       const COLON_STATE_COMMENTDASH = 6;
+       const COLON_STATE_COMMENTDASHDASH = 7;
+
+       // Allowed values for $this->mOutputType
+       // Parameter to startExternalParse().
+       const OT_HTML = 1;
+       const OT_WIKI = 2;
+       const OT_PREPROCESS = 3;
+       const OT_MSG = 4;
+
+       /**#@+
+        * @private
+        */
+       # Persistent:
+       var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
+               $mImageParams, $mImageParamsMagicArray, $mExtLinkBracketedRegex;
+
+       # Cleared with clearState():
+       var $mOutput, $mAutonumber, $mDTopen, $mStripState;
+       var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
+       var $mInterwikiLinkHolders, $mLinkHolders, $mUniqPrefix;
+       var $mIncludeSizes, $mDefaultSort;
+       var $mTemplates,        // cache of already loaded templates, avoids
+                               // multiple SQL queries for the same string
+           $mTemplatePath;     // stores an unsorted hash of all the templates already loaded
+                               // in this path. Used for loop detection.
+
+       # Temporary
+       # These are variables reset at least once per parse regardless of $clearState
+       var $mOptions,      // ParserOptions object
+               $mTitle,        // Title context, used for self-link rendering and similar things
+               $mOutputType,   // Output type, one of the OT_xxx constants
+               $ot,            // Shortcut alias, see setOutputType()
+               $mRevisionId,   // ID to display in {{REVISIONID}} tags
+               $mRevisionTimestamp, // The timestamp of the specified revision ID
+               $mRevIdForTs;   // The revision ID which was used to fetch the timestamp
+
+       /**#@-*/
+
+       /**
+        * Constructor
+        *
+        * @public
+        */
+       function __construct( $conf = array() ) {
+               $this->mTagHooks = array();
+               $this->mTransparentTagHooks = array();
+               $this->mFunctionHooks = array();
+               $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
+               $this->mFirstCall = true;
+               $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
+                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
+       }
+
+       /**
+        * Do various kinds of initialisation on the first call of the parser
+        */
+       function firstCallInit() {
+               if ( !$this->mFirstCall ) {
+                       return;
+               }
+               $this->mFirstCall = false;
+
+               wfProfileIn( __METHOD__ );
+               global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
+
+               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
+
+               # Syntax for arguments (see self::setFunctionHook):
+               #  "name for lookup in localized magic words array",
+               #  function callback,
+               #  optional SFH_NO_HASH to omit the hash from calls (e.g. {{int:...}
+               #    instead of {{#int:...}})
+               $this->setFunctionHook( 'int',              array( 'CoreParserFunctions', 'intFunction'      ), SFH_NO_HASH );
+               $this->setFunctionHook( 'ns',               array( 'CoreParserFunctions', 'ns'               ), SFH_NO_HASH );
+               $this->setFunctionHook( 'urlencode',        array( 'CoreParserFunctions', 'urlencode'        ), SFH_NO_HASH );
+               $this->setFunctionHook( 'lcfirst',          array( 'CoreParserFunctions', 'lcfirst'          ), SFH_NO_HASH );
+               $this->setFunctionHook( 'ucfirst',          array( 'CoreParserFunctions', 'ucfirst'          ), SFH_NO_HASH );
+               $this->setFunctionHook( 'lc',               array( 'CoreParserFunctions', 'lc'               ), SFH_NO_HASH );
+               $this->setFunctionHook( 'uc',               array( 'CoreParserFunctions', 'uc'               ), SFH_NO_HASH );
+               $this->setFunctionHook( 'localurl',         array( 'CoreParserFunctions', 'localurl'         ), SFH_NO_HASH );
+               $this->setFunctionHook( 'localurle',        array( 'CoreParserFunctions', 'localurle'        ), SFH_NO_HASH );
+               $this->setFunctionHook( 'fullurl',          array( 'CoreParserFunctions', 'fullurl'          ), SFH_NO_HASH );
+               $this->setFunctionHook( 'fullurle',         array( 'CoreParserFunctions', 'fullurle'         ), SFH_NO_HASH );
+               $this->setFunctionHook( 'formatnum',        array( 'CoreParserFunctions', 'formatnum'        ), SFH_NO_HASH );
+               $this->setFunctionHook( 'grammar',          array( 'CoreParserFunctions', 'grammar'          ), SFH_NO_HASH );
+               $this->setFunctionHook( 'plural',           array( 'CoreParserFunctions', 'plural'           ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofpages',    array( 'CoreParserFunctions', 'numberofpages'    ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofusers',    array( 'CoreParserFunctions', 'numberofusers'    ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofarticles', array( 'CoreParserFunctions', 'numberofarticles' ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberoffiles',    array( 'CoreParserFunctions', 'numberoffiles'    ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofadmins',   array( 'CoreParserFunctions', 'numberofadmins'   ), SFH_NO_HASH );
+               $this->setFunctionHook( 'numberofedits',    array( 'CoreParserFunctions', 'numberofedits'    ), SFH_NO_HASH );
+               $this->setFunctionHook( 'language',         array( 'CoreParserFunctions', 'language'         ), SFH_NO_HASH );
+               $this->setFunctionHook( 'padleft',          array( 'CoreParserFunctions', 'padleft'          ), SFH_NO_HASH );
+               $this->setFunctionHook( 'padright',         array( 'CoreParserFunctions', 'padright'         ), SFH_NO_HASH );
+               $this->setFunctionHook( 'anchorencode',     array( 'CoreParserFunctions', 'anchorencode'     ), SFH_NO_HASH );
+               $this->setFunctionHook( 'special',          array( 'CoreParserFunctions', 'special'          ) );
+               $this->setFunctionHook( 'defaultsort',      array( 'CoreParserFunctions', 'defaultsort'      ), SFH_NO_HASH );
+               $this->setFunctionHook( 'filepath',         array( 'CoreParserFunctions', 'filepath'         ), SFH_NO_HASH );
+
+               if ( $wgAllowDisplayTitle ) {
+                       $this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
+               }
+               if ( $wgAllowSlowParserFunctions ) {
+                       $this->setFunctionHook( 'pagesinnamespace', array( 'CoreParserFunctions', 'pagesinnamespace' ), SFH_NO_HASH );
+               }
+
+               $this->initialiseVariables();
+
+               wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Clear Parser state
+        *
+        * @private
+        */
+       function clearState() {
+               wfProfileIn( __METHOD__ );
+               if ( $this->mFirstCall ) {
+                       $this->firstCallInit();
+               }
+               $this->mOutput = new ParserOutput;
+               $this->mAutonumber = 0;
+               $this->mLastSection = '';
+               $this->mDTopen = false;
+               $this->mIncludeCount = array();
+               $this->mStripState = new StripState;
+               $this->mArgStack = array();
+               $this->mInPre = false;
+               $this->mInterwikiLinkHolders = array(
+                       'texts' => array(),
+                       'titles' => array()
+               );
+               $this->mLinkHolders = array(
+                       'namespaces' => array(),
+                       'dbkeys' => array(),
+                       'queries' => array(),
+                       'texts' => array(),
+                       'titles' => array()
+               );
+               $this->mRevisionTimestamp = $this->mRevisionId = null;
+
+               /**
+                * Prefix for temporary replacement strings for the multipass parser.
+                * \x07 should never appear in input as it's disallowed in XML.
+                * Using it at the front also gives us a little extra robustness
+                * since it shouldn't match when butted up against identifier-like
+                * string constructs.
+                */
+               $this->mUniqPrefix = "\x07UNIQ" . self::getRandomString();
+
+               # Clear these on every parse, bug 4549
+               $this->mTemplates = array();
+               $this->mTemplatePath = array();
+
+               $this->mShowToc = true;
+               $this->mForceTocPosition = false;
+               $this->mIncludeSizes = array(
+                       'pre-expand' => 0,
+                       'post-expand' => 0,
+                       'arg' => 0
+               );
+               $this->mDefaultSort = false;
+
+               wfRunHooks( 'ParserClearState', array( &$this ) );
+               wfProfileOut( __METHOD__ );
+       }
+
+       function setOutputType( $ot ) {
+               $this->mOutputType = $ot;
+               // Shortcut alias
+               $this->ot = array(
+                       'html' => $ot == self::OT_HTML,
+                       'wiki' => $ot == self::OT_WIKI,
+                       'msg' => $ot == self::OT_MSG,
+                       'pre' => $ot == self::OT_PREPROCESS,
+               );
+       }
+
+       /**
+        * Accessor for mUniqPrefix.
+        *
+        * @public
+        */
+       function uniqPrefix() {
+               return $this->mUniqPrefix;
+       }
+
+       /**
+        * Convert wikitext to HTML
+        * Do not call this function recursively.
+        *
+        * @param string $text Text we want to parse
+        * @param Title &$title A title object
+        * @param array $options
+        * @param boolean $linestart
+        * @param boolean $clearState
+        * @param int $revid number to pass in {{REVISIONID}}
+        * @return ParserOutput a ParserOutput
+        */
+       public function parse( $text, &$title, $options, $linestart = true, $clearState = true, $revid = null ) {
+               /**
+                * First pass--just handle <nowiki> sections, pass the rest off
+                * to internalParse() which does all the real work.
+                */
+
+               global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
+               $fname = 'Parser::parse-' . wfGetCaller();
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( $fname );
+
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+
+               $this->mOptions = $options;
+               $this->mTitle =& $title;
+               $oldRevisionId = $this->mRevisionId;
+               $oldRevisionTimestamp = $this->mRevisionTimestamp;
+               if( $revid !== null ) {
+                       $this->mRevisionId = $revid;
+                       $this->mRevisionTimestamp = null;
+               }
+               $this->setOutputType( self::OT_HTML );
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->strip( $text, $this->mStripState );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->internalParse( $text );
+               $text = $this->mStripState->unstripGeneral( $text );
+
+               # Clean up special characters, only run once, next-to-last before doBlockLevels
+               $fixtags = array(
+                       # french spaces, last one Guillemet-left
+                       # only if there is something before the space
+                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
+                       # french spaces, Guillemet-right
+                       '/(\\302\\253) /' => '\\1&nbsp;',
+               );
+               $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
+
+               # only once and last
+               $text = $this->doBlockLevels( $text, $linestart );
+
+               $this->replaceLinkHolders( $text );
+
+               # the position of the parserConvert() call should not be changed. it
+               # assumes that the links are all replaced and the only thing left
+               # is the <nowiki> mark.
+               # Side-effects: this calls $this->mOutput->setTitleText()
+               $text = $wgContLang->parserConvert( $text, $this );
+
+               $text = $this->mStripState->unstripNoWiki( $text );
+
+               wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) );
+
+//!JF Move to its own function
+
+               $uniq_prefix = $this->mUniqPrefix;
+                $matches = array();
+               $elements = array_keys( $this->mTransparentTagHooks );
+                $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+                foreach( $matches as $marker => $data ) {
+                        list( $element, $content, $params, $tag ) = $data;
+                        $tagName = strtolower( $element );
+                        if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+                                $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
+                                        array( $content, $params, $this ) );
+                        } else {
+                               $output = $tag;
+                       }
+                       $this->mStripState->general->setPair( $marker, $output );
+               }
+               $text = $this->mStripState->unstripGeneral( $text );
+
+               $text = Sanitizer::normalizeCharReferences( $text );
+
+               if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
+                       $text = self::tidy($text);
+               } else {
+                       # attempt to sanitize at least some nesting problems
+                       # (bug #2702 and quite a few others)
+                       $tidyregs = array(
+                               # ''Something [http://www.cool.com cool''] -->
+                               # <i>Something</i><a href="http://www.cool.com"..><i>cool></i></a>
+                               '/(<([bi])>)(<([bi])>)?([^<]*)(<\/?a[^<]*>)([^<]*)(<\/\\4>)?(<\/\\2>)/' =>
+                               '\\1\\3\\5\\8\\9\\6\\1\\3\\7\\8\\9',
+                               # fix up an anchor inside another anchor, only
+                               # at least for a single single nested link (bug 3695)
+                               '/(<a[^>]+>)([^<]*)(<a[^>]+>[^<]*)<\/a>(.*)<\/a>/' =>
+                               '\\1\\2</a>\\3</a>\\1\\4</a>',
+                               # fix div inside inline elements- doBlockLevels won't wrap a line which
+                               # contains a div, so fix it up here; replace
+                               # div with escaped text
+                               '/(<([aib]) [^>]+>)([^<]*)(<div([^>]*)>)(.*)(<\/div>)([^<]*)(<\/\\2>)/' =>
+                               '\\1\\3&lt;div\\5&gt;\\6&lt;/div&gt;\\8\\9',
+                               # remove empty italic or bold tag pairs, some
+                               # introduced by rules above
+                               '/<([bi])><\/\\1>/' => '',
+                       );
+
+                       $text = preg_replace(
+                               array_keys( $tidyregs ),
+                               array_values( $tidyregs ),
+                               $text );
+               }
+
+               wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
+
+               # Information on include size limits, for the benefit of users who try to skirt them
+               if ( $this->mOptions->getEnableLimitReport() ) {
+                       $max = $this->mOptions->getMaxIncludeSize();
+                       $limitReport =
+                               "Pre-expand include size: {$this->mIncludeSizes['pre-expand']}/$max bytes\n" .
+                               "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
+                               "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n";
+                       wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
+                       $text .= "<!-- \n$limitReport-->\n";
+               }
+               $this->mOutput->setText( $text );
+               $this->mRevisionId = $oldRevisionId;
+               $this->mRevisionTimestamp = $oldRevisionTimestamp;
+               wfProfileOut( $fname );
+               wfProfileOut( __METHOD__ );
+
+               return $this->mOutput;
+       }
+
+       /**
+        * Recursive parser entry point that can be called from an extension tag
+        * hook.
+        */
+       function recursiveTagParse( $text ) {
+               wfProfileIn( __METHOD__ );
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->strip( $text, $this->mStripState );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->internalParse( $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Expand templates and variables in the text, producing valid, static wikitext.
+        * Also removes comments.
+        */
+       function preprocess( $text, $title, $options, $revid = null ) {
+               wfProfileIn( __METHOD__ );
+               $this->clearState();
+               $this->setOutputType( self::OT_PREPROCESS );
+               $this->mOptions = $options;
+               $this->mTitle = $title;
+               if( $revid !== null ) {
+                       $this->mRevisionId = $revid;
+               }
+               wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
+               $text = $this->strip( $text, $this->mStripState );
+               wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
+               if ( $this->mOptions->getRemoveComments() ) {
+                       $text = Sanitizer::removeHTMLcomments( $text );
+               }
+               $text = $this->replaceVariables( $text );
+               $text = $this->mStripState->unstripBoth( $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Get a random string
+        *
+        * @private
+        * @static
+        */
+       function getRandomString() {
+               return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
+       }
+
+       function &getTitle() { return $this->mTitle; }
+       function getOptions() { return $this->mOptions; }
+       function getRevisionId() { return $this->mRevisionId; }
+
+       function getFunctionLang() {
+               global $wgLang, $wgContLang;
+               return $this->mOptions->getInterfaceMessage() ? $wgLang : $wgContLang;
+       }
+
+       /**
+        * Replaces all occurrences of HTML-style comments and the given tags
+        * in the text with a random marker and returns teh next text. The output
+        * parameter $matches will be an associative array filled with data in
+        * the form:
+        *   'UNIQ-xxxxx' => array(
+        *     'element',
+        *     'tag content',
+        *     array( 'param' => 'x' ),
+        *     '<element param="x">tag content</element>' ) )
+        *
+        * @param $elements list of element names. Comments are always extracted.
+        * @param $text Source text string.
+        * @param $uniq_prefix
+        *
+        * @public
+        * @static
+        */
+       function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
+               static $n = 1;
+               $stripped = '';
+               $matches = array();
+
+               $taglist = implode( '|', $elements );
+               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
+
+               while ( '' != $text ) {
+                       $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
+                       $stripped .= $p[0];
+                       if( count( $p ) < 5 ) {
+                               break;
+                       }
+                       if( count( $p ) > 5 ) {
+                               // comment
+                               $element    = $p[4];
+                               $attributes = '';
+                               $close      = '';
+                               $inside     = $p[5];
+                       } else {
+                               // tag
+                               $element    = $p[1];
+                               $attributes = $p[2];
+                               $close      = $p[3];
+                               $inside     = $p[4];
+                       }
+
+                       $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07";
+                       $stripped .= $marker;
+
+                       if ( $close === '/>' ) {
+                               // Empty element tag, <tag />
+                               $content = null;
+                               $text = $inside;
+                               $tail = null;
+                       } else {
+                               if( $element == '!--' ) {
+                                       $end = '/(-->)/';
+                               } else {
+                                       $end = "/(<\\/$element\\s*>)/i";
+                               }
+                               $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
+                               $content = $q[0];
+                               if( count( $q ) < 3 ) {
+                                       # No end tag -- let it run out to the end of the text.
+                                       $tail = '';
+                                       $text = '';
+                               } else {
+                                       $tail = $q[1];
+                                       $text = $q[2];
+                               }
+                       }
+
+                       $matches[$marker] = array( $element,
+                               $content,
+                               Sanitizer::decodeTagAttributes( $attributes ),
+                               "<$element$attributes$close$content$tail" );
+               }
+               return $stripped;
+       }
+
+       /**
+        * Strips and renders nowiki, pre, math, hiero
+        * If $render is set, performs necessary rendering operations on plugins
+        * Returns the text, and fills an array with data needed in unstrip()
+        *
+        * @param StripState $state
+        *
+        * @param bool $stripcomments when set, HTML comments <!-- like this -->
+        *  will be stripped in addition to other tags. This is important
+        *  for section editing, where these comments cause confusion when
+        *  counting the sections in the wikisource
+        *
+        * @param array dontstrip contains tags which should not be stripped;
+        *  used to prevent stipping of <gallery> when saving (fixes bug 2700)
+        *
+        * @private
+        */
+       function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
+               global $wgContLang;
+               wfProfileIn( __METHOD__ );
+               $render = ($this->mOutputType == self::OT_HTML);
+
+               $uniq_prefix = $this->mUniqPrefix;
+               $commentState = new ReplacementArray;
+               $nowikiItems = array();
+               $generalItems = array();
+
+               $elements = array_merge(
+                       array( 'nowiki', 'gallery' ),
+                       array_keys( $this->mTagHooks ) );
+               global $wgRawHtml;
+               if( $wgRawHtml ) {
+                       $elements[] = 'html';
+               }
+               if( $this->mOptions->getUseTeX() ) {
+                       $elements[] = 'math';
+               }
+
+               # Removing $dontstrip tags from $elements list (currently only 'gallery', fixing bug 2700)
+               foreach ( $elements AS $k => $v ) {
+                       if ( !in_array ( $v , $dontstrip ) ) continue;
+                       unset ( $elements[$k] );
+               }
+
+               $matches = array();
+               $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+
+               foreach( $matches as $marker => $data ) {
+                       list( $element, $content, $params, $tag ) = $data;
+                       if( $render ) {
+                               $tagName = strtolower( $element );
+                               wfProfileIn( __METHOD__."-render-$tagName" );
+                               switch( $tagName ) {
+                               case '!--':
+                                       // Comment
+                                       if( substr( $tag, -3 ) == '-->' ) {
+                                               $output = $tag;
+                                       } else {
+                                               // Unclosed comment in input.
+                                               // Close it so later stripping can remove it
+                                               $output = "$tag-->";
+                                       }
+                                       break;
+                               case 'html':
+                                       if( $wgRawHtml ) {
+                                               $output = $content;
+                                               break;
+                                       }
+                                       // Shouldn't happen otherwise. :)
+                               case 'nowiki':
+                                       $output = Xml::escapeTagsOnly( $content );
+                                       break;
+                               case 'math':
+                                       $output = $wgContLang->armourMath(
+                                               MathRenderer::renderMath( $content, $params ) );
+                                       break;
+                               case 'gallery':
+                                       $output = $this->renderImageGallery( $content, $params );
+                                       break;
+                               default:
+                                       if( isset( $this->mTagHooks[$tagName] ) ) {
+                                               $output = call_user_func_array( $this->mTagHooks[$tagName],
+                                                       array( $content, $params, $this ) );
+                                       } else {
+                                               throw new MWException( "Invalid call hook $element" );
+                                       }
+                               }
+                               wfProfileOut( __METHOD__."-render-$tagName" );
+                       } else {
+                               // Just stripping tags; keep the source
+                               $output = $tag;
+                       }
+
+                       // Unstrip the output, to support recursive strip() calls
+                       $output = $state->unstripBoth( $output );
+
+                       if( !$stripcomments && $element == '!--' ) {
+                               $commentState->setPair( $marker, $output );
+                       } elseif ( $element == 'html' || $element == 'nowiki' ) {
+                               $nowikiItems[$marker] = $output;
+                       } else {
+                               $generalItems[$marker] = $output;
+                       }
+               }
+               # Add the new items to the state
+               # We do this after the loop instead of during it to avoid slowing
+               # down the recursive unstrip
+               $state->nowiki->mergeArray( $nowikiItems );
+               $state->general->mergeArray( $generalItems );
+
+               # Unstrip comments unless explicitly told otherwise.
+               # (The comments are always stripped prior to this point, so as to
+               # not invoke any extension tags / parser hooks contained within
+               # a comment.)
+               if ( !$stripcomments ) {
+                       // Put them all back and forget them
+                       $text = $commentState->replace( $text );
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Restores pre, math, and other extensions removed by strip()
+        *
+        * always call unstripNoWiki() after this one
+        * @private
+        * @deprecated use $this->mStripState->unstrip()
+        */
+       function unstrip( $text, $state ) {
+               return $state->unstripGeneral( $text );
+       }
+
+       /**
+        * Always call this after unstrip() to preserve the order
+        *
+        * @private
+        * @deprecated use $this->mStripState->unstrip()
+        */
+       function unstripNoWiki( $text, $state ) {
+               return $state->unstripNoWiki( $text );
+       }
+
+       /**
+        * @deprecated use $this->mStripState->unstripBoth()
+        */
+       function unstripForHTML( $text ) {
+               return $this->mStripState->unstripBoth( $text );
+       }
+
+       /**
+        * Add an item to the strip state
+        * Returns the unique tag which must be inserted into the stripped text
+        * The tag will be replaced with the original text in unstrip()
+        *
+        * @private
+        */
+       function insertStripItem( $text, &$state ) {
+               $rnd = $this->mUniqPrefix . '-item' . self::getRandomString();
+               $state->general->setPair( $rnd, $text );
+               return $rnd;
+       }
+
+       /**
+        * Interface with html tidy, used if $wgUseTidy = true.
+        * If tidy isn't able to correct the markup, the original will be
+        * returned in all its glory with a warning comment appended.
+        *
+        * Either the external tidy program or the in-process tidy extension
+        * will be used depending on availability. Override the default
+        * $wgTidyInternal setting to disable the internal if it's not working.
+        *
+        * @param string $text Hideous HTML input
+        * @return string Corrected HTML output
+        * @public
+        * @static
+        */
+       function tidy( $text ) {
+               global $wgTidyInternal;
+               $wrappedtext = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'.
+' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
+'<head><title>test</title></head><body>'.$text.'</body></html>';
+               if( $wgTidyInternal ) {
+                       $correctedtext = self::internalTidy( $wrappedtext );
+               } else {
+                       $correctedtext = self::externalTidy( $wrappedtext );
+               }
+               if( is_null( $correctedtext ) ) {
+                       wfDebug( "Tidy error detected!\n" );
+                       return $text . "\n<!-- Tidy found serious XHTML errors -->\n";
+               }
+               return $correctedtext;
+       }
+
+       /**
+        * Spawn an external HTML tidy process and get corrected markup back from it.
+        *
+        * @private
+        * @static
+        */
+       function externalTidy( $text ) {
+               global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
+               $fname = 'Parser::externalTidy';
+               wfProfileIn( $fname );
+
+               $cleansource = '';
+               $opts = ' -utf8';
+
+               $descriptorspec = array(
+                       0 => array('pipe', 'r'),
+                       1 => array('pipe', 'w'),
+                       2 => array('file', wfGetNull(), 'a')
+               );
+               $pipes = array();
+               $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes);
+               if (is_resource($process)) {
+                       // Theoretically, this style of communication could cause a deadlock
+                       // here. If the stdout buffer fills up, then writes to stdin could
+                       // block. This doesn't appear to happen with tidy, because tidy only
+                       // writes to stdout after it's finished reading from stdin. Search
+                       // for tidyParseStdin and tidySaveStdout in console/tidy.c
+                       fwrite($pipes[0], $text);
+                       fclose($pipes[0]);
+                       while (!feof($pipes[1])) {
+                               $cleansource .= fgets($pipes[1], 1024);
+                       }
+                       fclose($pipes[1]);
+                       proc_close($process);
+               }
+
+               wfProfileOut( $fname );
+
+               if( $cleansource == '' && $text != '') {
+                       // Some kind of error happened, so we couldn't get the corrected text.
+                       // Just give up; we'll use the source text and append a warning.
+                       return null;
+               } else {
+                       return $cleansource;
+               }
+       }
+
+       /**
+        * Use the HTML tidy PECL extension to use the tidy library in-process,
+        * saving the overhead of spawning a new process.
+        *
+        * 'pear install tidy' should be able to compile the extension module.
+        *
+        * @private
+        * @static
+        */
+       function internalTidy( $text ) {
+               global $wgTidyConf, $IP;
+               $fname = 'Parser::internalTidy';
+               wfProfileIn( $fname );
+
+               $tidy = new tidy;
+               $tidy->parseString( $text, $wgTidyConf, 'utf8' );
+               $tidy->cleanRepair();
+               if( $tidy->getStatus() == 2 ) {
+                       // 2 is magic number for fatal error
+                       // http://www.php.net/manual/en/function.tidy-get-status.php
+                       $cleansource = null;
+               } else {
+                       $cleansource = tidy_get_output( $tidy );
+               }
+               wfProfileOut( $fname );
+               return $cleansource;
+       }
+
+       /**
+        * parse the wiki syntax used to render tables
+        *
+        * @private
+        */
+       function doTableStuff ( $text ) {
+               $fname = 'Parser::doTableStuff';
+               wfProfileIn( $fname );
+
+               $lines = explode ( "\n" , $text );
+               $td_history = array (); // Is currently a td tag open?
+               $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
+               $tr_history = array (); // Is currently a tr tag open?
+               $tr_attributes = array (); // history of tr attributes
+               $has_opened_tr = array(); // Did this table open a <tr> element?
+               $indent_level = 0; // indent level of the table
+               foreach ( $lines as $key => $line )
+               {
+                       $line = trim ( $line );
+
+                       if( $line == '' ) { // empty line, go to next line
+                               continue;
+                       }
+                       $first_character = $line{0};
+                       $matches = array();
+
+                       if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
+                               // First check if we are starting a new table
+                               $indent_level = strlen( $matches[1] );
+
+                               $attributes = $this->mStripState->unstripBoth( $matches[2] );
+                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
+
+                               $lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
+                               array_push ( $td_history , false );
+                               array_push ( $last_tag_history , '' );
+                               array_push ( $tr_history , false );
+                               array_push ( $tr_attributes , '' );
+                               array_push ( $has_opened_tr , false );
+                       } else if ( count ( $td_history ) == 0 ) {
+                               // Don't do any of the following
+                               continue;
+                       } else if ( substr ( $line , 0 , 2 ) == '|}' ) {
+                               // We are ending a table
+                               $line = '</table>' . substr ( $line , 2 );
+                               $last_tag = array_pop ( $last_tag_history );
+
+                               if ( !array_pop ( $has_opened_tr ) ) {
+                                       $line = "<tr><td></td></tr>{$line}";
+                               }
+
+                               if ( array_pop ( $tr_history ) ) {
+                                       $line = "</tr>{$line}";
+                               }
+
+                               if ( array_pop ( $td_history ) ) {
+                                       $line = "</{$last_tag}>{$line}";
+                               }
+                               array_pop ( $tr_attributes );
+                               $lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
+                       } else if ( substr ( $line , 0 , 2 ) == '|-' ) {
+                               // Now we have a table row
+                               $line = preg_replace( '#^\|-+#', '', $line );
+
+                               // Whats after the tag is now only attributes
+                               $attributes = $this->mStripState->unstripBoth( $line );
+                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
+                               array_pop ( $tr_attributes );
+                               array_push ( $tr_attributes , $attributes );
+
+                               $line = '';
+                               $last_tag = array_pop ( $last_tag_history );
+                               array_pop ( $has_opened_tr );
+                               array_push ( $has_opened_tr , true );
+
+                               if ( array_pop ( $tr_history ) ) {
+                                       $line = '</tr>';
+                               }
+
+                               if ( array_pop ( $td_history ) ) {
+                                       $line = "</{$last_tag}>{$line}";
+                               }
+
+                               $lines[$key] = $line;
+                               array_push ( $tr_history , false );
+                               array_push ( $td_history , false );
+                               array_push ( $last_tag_history , '' );
+                       }
+                       else if ( $first_character == '|' || $first_character == '!' || substr ( $line , 0 , 2 )  == '|+' ) {
+                               // This might be cell elements, td, th or captions
+                               if ( substr ( $line , 0 , 2 ) == '|+' ) {
+                                       $first_character = '+';
+                                       $line = substr ( $line , 1 );
+                               }
+
+                               $line = substr ( $line , 1 );
+
+                               if ( $first_character == '!' ) {
+                                       $line = str_replace ( '!!' , '||' , $line );
+                               }
+
+                               // Split up multiple cells on the same line.
+                               // FIXME : This can result in improper nesting of tags processed
+                               // by earlier parser steps, but should avoid splitting up eg
+                               // attribute values containing literal "||".
+                               $cells = StringUtils::explodeMarkup( '||' , $line );
+
+                               $lines[$key] = '';
+
+                               // Loop through each table cell
+                               foreach ( $cells as $cell )
+                               {
+                                       $previous = '';
+                                       if ( $first_character != '+' )
+                                       {
+                                               $tr_after = array_pop ( $tr_attributes );
+                                               if ( !array_pop ( $tr_history ) ) {
+                                                       $previous = "<tr{$tr_after}>\n";
+                                               }
+                                               array_push ( $tr_history , true );
+                                               array_push ( $tr_attributes , '' );
+                                               array_pop ( $has_opened_tr );
+                                               array_push ( $has_opened_tr , true );
+                                       }
+
+                                       $last_tag = array_pop ( $last_tag_history );
+
+                                       if ( array_pop ( $td_history ) ) {
+                                               $previous = "</{$last_tag}>{$previous}";
+                                       }
+
+                                       if ( $first_character == '|' ) {
+                                               $last_tag = 'td';
+                                       } else if ( $first_character == '!' ) {
+                                               $last_tag = 'th';
+                                       } else if ( $first_character == '+' ) {
+                                               $last_tag = 'caption';
+                                       } else {
+                                               $last_tag = '';
+                                       }
+
+                                       array_push ( $last_tag_history , $last_tag );
+
+                                       // A cell could contain both parameters and data
+                                       $cell_data = explode ( '|' , $cell , 2 );
+
+                                       // Bug 553: Note that a '|' inside an invalid link should not
+                                       // be mistaken as delimiting cell parameters
+                                       if ( strpos( $cell_data[0], '[[' ) !== false ) {
+                                               $cell = "{$previous}<{$last_tag}>{$cell}";
+                                       } else if ( count ( $cell_data ) == 1 )
+                                               $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
+                                       else {
+                                               $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
+                                               $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
+                                               $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
+                                       }
+
+                                       $lines[$key] .= $cell;
+                                       array_push ( $td_history , true );
+                               }
+                       }
+               }
+
+               // Closing open td, tr && table
+               while ( count ( $td_history ) > 0 )
+               {
+                       if ( array_pop ( $td_history ) ) {
+                               $lines[] = '</td>' ;
+                       }
+                       if ( array_pop ( $tr_history ) ) {
+                               $lines[] = '</tr>' ;
+                       }
+                       if ( !array_pop ( $has_opened_tr ) ) {
+                               $lines[] = "<tr><td></td></tr>" ;
+                       }
+
+                       $lines[] = '</table>' ;
+               }
+
+               $output = implode ( "\n" , $lines ) ;
+
+               // special case: don't return empty table
+               if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
+                       $output = '';
+               }
+
+               wfProfileOut( $fname );
+
+               return $output;
+       }
+
+       /**
+        * Helper function for parse() that transforms wiki markup into
+        * HTML. Only called for $mOutputType == OT_HTML.
+        *
+        * @private
+        */
+       function internalParse( $text ) {
+               $args = array();
+               $isMain = true;
+               $fname = 'Parser::internalParse';
+               wfProfileIn( $fname );
+
+               # Hook to suspend the parser in this state
+               if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
+                       wfProfileOut( $fname );
+                       return $text ;
+               }
+
+               # Remove <noinclude> tags and <includeonly> sections
+               $text = strtr( $text, array( '<onlyinclude>' => '' , '</onlyinclude>' => '' ) );
+               $text = strtr( $text, array( '<noinclude>' => '', '</noinclude>' => '') );
+               $text = StringUtils::delimiterReplace( '<includeonly>', '</includeonly>', '', $text );
+
+               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) );
+
+               $text = $this->replaceVariables( $text, $args );
+               wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
+
+               // Tables need to come after variable replacement for things to work
+               // properly; putting them before other transformations should keep
+               // exciting things like link expansions from showing up in surprising
+               // places.
+               $text = $this->doTableStuff( $text );
+
+               $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
+
+               $text = $this->stripToc( $text );
+               $this->stripNoGallery( $text );
+               $text = $this->doHeadings( $text );
+               if($this->mOptions->getUseDynamicDates()) {
+                       $df =& DateFormatter::getInstance();
+                       $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
+               }
+               $text = $this->doAllQuotes( $text );
+               $text = $this->replaceInternalLinks( $text );
+               $text = $this->replaceExternalLinks( $text );
+
+               # replaceInternalLinks may sometimes leave behind
+               # absolute URLs, which have to be masked to hide them from replaceExternalLinks
+               $text = str_replace($this->mUniqPrefix."NOPARSE", "", $text);
+
+               $text = $this->doMagicLinks( $text );
+               $text = $this->formatHeadings( $text, $isMain );
+
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * Replace special strings like "ISBN xxx" and "RFC xxx" with
+        * magic external links.
+        *
+        * @private
+        */
+       function &doMagicLinks( &$text ) {
+               wfProfileIn( __METHOD__ );
+               $text = preg_replace_callback(
+                       '!(?:                           # Start cases
+                           <a.*?</a> |                 # Skip link text
+                           <.*?> |                     # Skip stuff inside HTML elements
+                           (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
+                           ISBN\s+(\b                  # ISBN, capture number as m[2]
+                                     (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
+                                     (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
+                                     [0-9Xx]                 # check digit
+                                   \b)
+                       )!x', array( &$this, 'magicLinkCallback' ), $text );
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       function magicLinkCallback( $m ) {
+               if ( substr( $m[0], 0, 1 ) == '<' ) {
+                       # Skip HTML element
+                       return $m[0];
+               } elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
+                       $isbn = $m[2];
+                       $num = strtr( $isbn, array(
+                               '-' => '',
+                               ' ' => '',
+                               'x' => 'X',
+                       ));
+                       $titleObj = SpecialPage::getTitleFor( 'Booksources' );
+                       $text = '<a href="' .
+                               $titleObj->escapeLocalUrl( "isbn=$num" ) .
+                               "\" class=\"internal\">ISBN $isbn</a>";
+               } else {
+                       if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
+                               $keyword = 'RFC';
+                               $urlmsg = 'rfcurl';
+                               $id = $m[1];
+                       } elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
+                               $keyword = 'PMID';
+                               $urlmsg = 'pubmedurl';
+                               $id = $m[1];
+                       } else {
+                               throw new MWException( __METHOD__.': unrecognised match type "' .
+                                       substr($m[0], 0, 20 ) . '"' );
+                       }
+
+                       $url = wfMsg( $urlmsg, $id);
+                       $sk = $this->mOptions->getSkin();
+                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+                       $text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+               }
+               return $text;
+       }
+
+       /**
+        * Parse headers and return html
+        *
+        * @private
+        */
+       function doHeadings( $text ) {
+               $fname = 'Parser::doHeadings';
+               wfProfileIn( $fname );
+               for ( $i = 6; $i >= 1; --$i ) {
+                       $h = str_repeat( '=', $i );
+                       $text = preg_replace( "/^{$h}(.+){$h}\\s*$/m",
+                         "<h{$i}>\\1</h{$i}>\\2", $text );
+               }
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * Replace single quotes with HTML markup
+        * @private
+        * @return string the altered text
+        */
+       function doAllQuotes( $text ) {
+               $fname = 'Parser::doAllQuotes';
+               wfProfileIn( $fname );
+               $outtext = '';
+               $lines = explode( "\n", $text );
+               foreach ( $lines as $line ) {
+                       $outtext .= $this->doQuotes ( $line ) . "\n";
+               }
+               $outtext = substr($outtext, 0,-1);
+               wfProfileOut( $fname );
+               return $outtext;
+       }
+
+       /**
+        * Helper function for doAllQuotes()
+        */
+       public function doQuotes( $text ) {
+               $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+               if ( count( $arr ) == 1 )
+                       return $text;
+               else
+               {
+                       # First, do some preliminary work. This may shift some apostrophes from
+                       # being mark-up to being text. It also counts the number of occurrences
+                       # of bold and italics mark-ups.
+                       $i = 0;
+                       $numbold = 0;
+                       $numitalics = 0;
+                       foreach ( $arr as $r )
+                       {
+                               if ( ( $i % 2 ) == 1 )
+                               {
+                                       # If there are ever four apostrophes, assume the first is supposed to
+                                       # be text, and the remaining three constitute mark-up for bold text.
+                                       if ( strlen( $arr[$i] ) == 4 )
+                                       {
+                                               $arr[$i-1] .= "'";
+                                               $arr[$i] = "'''";
+                                       }
+                                       # If there are more than 5 apostrophes in a row, assume they're all
+                                       # text except for the last 5.
+                                       else if ( strlen( $arr[$i] ) > 5 )
+                                       {
+                                               $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
+                                               $arr[$i] = "'''''";
+                                       }
+                                       # Count the number of occurrences of bold and italics mark-ups.
+                                       # We are not counting sequences of five apostrophes.
+                                       if ( strlen( $arr[$i] ) == 2 )      { $numitalics++;             }
+                                       else if ( strlen( $arr[$i] ) == 3 ) { $numbold++;                }
+                                       else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
+                               }
+                               $i++;
+                       }
+
+                       # If there is an odd number of both bold and italics, it is likely
+                       # that one of the bold ones was meant to be an apostrophe followed
+                       # by italics. Which one we cannot know for certain, but it is more
+                       # likely to be one that has a single-letter word before it.
+                       if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
+                       {
+                               $i = 0;
+                               $firstsingleletterword = -1;
+                               $firstmultiletterword = -1;
+                               $firstspace = -1;
+                               foreach ( $arr as $r )
+                               {
+                                       if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
+                                       {
+                                               $x1 = substr ($arr[$i-1], -1);
+                                               $x2 = substr ($arr[$i-1], -2, 1);
+                                               if ($x1 == ' ') {
+                                                       if ($firstspace == -1) $firstspace = $i;
+                                               } else if ($x2 == ' ') {
+                                                       if ($firstsingleletterword == -1) $firstsingleletterword = $i;
+                                               } else {
+                                                       if ($firstmultiletterword == -1) $firstmultiletterword = $i;
+                                               }
+                                       }
+                                       $i++;
+                               }
+
+                               # If there is a single-letter word, use it!
+                               if ($firstsingleletterword > -1)
+                               {
+                                       $arr [ $firstsingleletterword ] = "''";
+                                       $arr [ $firstsingleletterword-1 ] .= "'";
+                               }
+                               # If not, but there's a multi-letter word, use that one.
+                               else if ($firstmultiletterword > -1)
+                               {
+                                       $arr [ $firstmultiletterword ] = "''";
+                                       $arr [ $firstmultiletterword-1 ] .= "'";
+                               }
+                               # ... otherwise use the first one that has neither.
+                               # (notice that it is possible for all three to be -1 if, for example,
+                               # there is only one pentuple-apostrophe in the line)
+                               else if ($firstspace > -1)
+                               {
+                                       $arr [ $firstspace ] = "''";
+                                       $arr [ $firstspace-1 ] .= "'";
+                               }
+                       }
+
+                       # Now let's actually convert our apostrophic mush to HTML!
+                       $output = '';
+                       $buffer = '';
+                       $state = '';
+                       $i = 0;
+                       foreach ($arr as $r)
+                       {
+                               if (($i % 2) == 0)
+                               {
+                                       if ($state == 'both')
+                                               $buffer .= $r;
+                                       else
+                                               $output .= $r;
+                               }
+                               else
+                               {
+                                       if (strlen ($r) == 2)
+                                       {
+                                               if ($state == 'i')
+                                               { $output .= '</i>'; $state = ''; }
+                                               else if ($state == 'bi')
+                                               { $output .= '</i>'; $state = 'b'; }
+                                               else if ($state == 'ib')
+                                               { $output .= '</b></i><b>'; $state = 'b'; }
+                                               else if ($state == 'both')
+                                               { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
+                                               else # $state can be 'b' or ''
+                                               { $output .= '<i>'; $state .= 'i'; }
+                                       }
+                                       else if (strlen ($r) == 3)
+                                       {
+                                               if ($state == 'b')
+                                               { $output .= '</b>'; $state = ''; }
+                                               else if ($state == 'bi')
+                                               { $output .= '</i></b><i>'; $state = 'i'; }
+                                               else if ($state == 'ib')
+                                               { $output .= '</b>'; $state = 'i'; }
+                                               else if ($state == 'both')
+                                               { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
+                                               else # $state can be 'i' or ''
+                                               { $output .= '<b>'; $state .= 'b'; }
+                                       }
+                                       else if (strlen ($r) == 5)
+                                       {
+                                               if ($state == 'b')
+                                               { $output .= '</b><i>'; $state = 'i'; }
+                                               else if ($state == 'i')
+                                               { $output .= '</i><b>'; $state = 'b'; }
+                                               else if ($state == 'bi')
+                                               { $output .= '</i></b>'; $state = ''; }
+                                               else if ($state == 'ib')
+                                               { $output .= '</b></i>'; $state = ''; }
+                                               else if ($state == 'both')
+                                               { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
+                                               else # ($state == '')
+                                               { $buffer = ''; $state = 'both'; }
+                                       }
+                               }
+                               $i++;
+                       }
+                       # Now close all remaining tags.  Notice that the order is important.
+                       if ($state == 'b' || $state == 'ib')
+                               $output .= '</b>';
+                       if ($state == 'i' || $state == 'bi' || $state == 'ib')
+                               $output .= '</i>';
+                       if ($state == 'bi')
+                               $output .= '</b>';
+                       # There might be lonely ''''', so make sure we have a buffer
+                       if ($state == 'both' && $buffer)
+                               $output .= '<b><i>'.$buffer.'</i></b>';
+                       return $output;
+               }
+       }
+
+       /**
+        * Replace external links
+        *
+        * Note: this is all very hackish and the order of execution matters a lot.
+        * Make sure to run maintenance/parserTests.php if you change this code.
+        *
+        * @private
+        */
+       function replaceExternalLinks( $text ) {
+               global $wgContLang;
+               $fname = 'Parser::replaceExternalLinks';
+               wfProfileIn( $fname );
+
+               $sk = $this->mOptions->getSkin();
+
+               $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+
+               $s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
+
+               $i = 0;
+               while ( $i<count( $bits ) ) {
+                       $url = $bits[$i++];
+                       $protocol = $bits[$i++];
+                       $text = $bits[$i++];
+                       $trail = $bits[$i++];
+
+                       # The characters '<' and '>' (which were escaped by
+                       # removeHTMLtags()) should not be included in
+                       # URLs, per RFC 2396.
+                       $m2 = array();
+                       if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+                               $text = substr($url, $m2[0][1]) . ' ' . $text;
+                               $url = substr($url, 0, $m2[0][1]);
+                       }
+
+                       # If the link text is an image URL, replace it with an <img> tag
+                       # This happened by accident in the original parser, but some people used it extensively
+                       $img = $this->maybeMakeExternalImage( $text );
+                       if ( $img !== false ) {
+                               $text = $img;
+                       }
+
+                       $dtrail = '';
+
+                       # Set linktype for CSS - if URL==text, link is essentially free
+                       $linktype = ($text == $url) ? 'free' : 'text';
+
+                       # No link text, e.g. [http://domain.tld/some.link]
+                       if ( $text == '' ) {
+                               # Autonumber if allowed. See bug #5918
+                               if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
+                                       $text = '[' . ++$this->mAutonumber . ']';
+                                       $linktype = 'autonumber';
+                               } else {
+                                       # Otherwise just use the URL
+                                       $text = htmlspecialchars( $url );
+                                       $linktype = 'free';
+                               }
+                       } else {
+                               # Have link text, e.g. [http://domain.tld/some.link text]s
+                               # Check for trail
+                               list( $dtrail, $trail ) = Linker::splitTrail( $trail );
+                       }
+
+                       $text = $wgContLang->markNoConversion($text);
+
+                       $url = Sanitizer::cleanUrl( $url );
+
+                       # Process the trail (i.e. everything after this link up until start of the next link),
+                       # replacing any non-bracketed links
+                       $trail = $this->replaceFreeExternalLinks( $trail );
+
+                       # Use the encoded URL
+                       # This means that users can paste URLs directly into the text
+                       # Funny characters like &ouml; aren't valid in URLs anyway
+                       # This was changed in August 2004
+                       $s .= $sk->makeExternalLink( $url, $text, false, $linktype, $this->mTitle->getNamespace() ) . $dtrail . $trail;
+
+                       # Register link in the output object.
+                       # Replace unnecessary URL escape codes with the referenced character
+                       # This prevents spammers from hiding links from the filters
+                       $pasteurized = self::replaceUnusualEscapes( $url );
+                       $this->mOutput->addExternalLink( $pasteurized );
+               }
+
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /**
+        * Replace anything that looks like a URL with a link
+        * @private
+        */
+       function replaceFreeExternalLinks( $text ) {
+               global $wgContLang;
+               $fname = 'Parser::replaceFreeExternalLinks';
+               wfProfileIn( $fname );
+
+               $bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+               $s = array_shift( $bits );
+               $i = 0;
+
+               $sk = $this->mOptions->getSkin();
+
+               while ( $i < count( $bits ) ){
+                       $protocol = $bits[$i++];
+                       $remainder = $bits[$i++];
+
+                       $m = array();
+                       if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
+                               # Found some characters after the protocol that look promising
+                               $url = $protocol . $m[1];
+                               $trail = $m[2];
+
+                               # special case: handle urls as url args:
+                               # http://www.example.com/foo?=http://www.example.com/bar
+                               if(strlen($trail) == 0 &&
+                                       isset($bits[$i]) &&
+                                       preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
+                                       preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
+                               {
+                                       # add protocol, arg
+                                       $url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
+                                       $i += 2;
+                                       $trail = $m[2];
+                               }
+
+                               # The characters '<' and '>' (which were escaped by
+                               # removeHTMLtags()) should not be included in
+                               # URLs, per RFC 2396.
+                               $m2 = array();
+                               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
+                                       $trail = substr($url, $m2[0][1]) . $trail;
+                                       $url = substr($url, 0, $m2[0][1]);
+                               }
+
+                               # Move trailing punctuation to $trail
+                               $sep = ',;\.:!?';
+                               # If there is no left bracket, then consider right brackets fair game too
+                               if ( strpos( $url, '(' ) === false ) {
+                                       $sep .= ')';
+                               }
+
+                               $numSepChars = strspn( strrev( $url ), $sep );
+                               if ( $numSepChars ) {
+                                       $trail = substr( $url, -$numSepChars ) . $trail;
+                                       $url = substr( $url, 0, -$numSepChars );
+                               }
+
+                               $url = Sanitizer::cleanUrl( $url );
+
+                               # Is this an external image?
+                               $text = $this->maybeMakeExternalImage( $url );
+                               if ( $text === false ) {
+                                       # Not an image, make a link
+                                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+                                       # Register it in the output object...
+                                       # Replace unnecessary URL escape codes with their equivalent characters
+                                       $pasteurized = self::replaceUnusualEscapes( $url );
+                                       $this->mOutput->addExternalLink( $pasteurized );
+                               }
+                               $s .= $text . $trail;
+                       } else {
+                               $s .= $protocol . $remainder;
+                       }
+               }
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /**
+        * Replace unusual URL escape codes with their equivalent characters
+        * @param string
+        * @return string
+        * @static
+        * @todo  This can merge genuinely required bits in the path or query string,
+        *        breaking legit URLs. A proper fix would treat the various parts of
+        *        the URL differently; as a workaround, just use the output for
+        *        statistical records, not for actual linking/output.
+        */
+       static function replaceUnusualEscapes( $url ) {
+               return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
+                       array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
+       }
+
+       /**
+        * Callback function used in replaceUnusualEscapes().
+        * Replaces unusual URL escape codes with their equivalent character
+        * @static
+        * @private
+        */
+       private static function replaceUnusualEscapesCallback( $matches ) {
+               $char = urldecode( $matches[0] );
+               $ord = ord( $char );
+               // Is it an unsafe or HTTP reserved character according to RFC 1738?
+               if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
+                       // No, shouldn't be escaped
+                       return $char;
+               } else {
+                       // Yes, leave it escaped
+                       return $matches[0];
+               }
+       }
+
+       /**
+        * make an image if it's allowed, either through the global
+        * option or through the exception
+        * @private
+        */
+       function maybeMakeExternalImage( $url ) {
+               $sk = $this->mOptions->getSkin();
+               $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
+               $imagesexception = !empty($imagesfrom);
+               $text = false;
+               if ( $this->mOptions->getAllowExternalImages()
+                    || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
+                       if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
+                               # Image found
+                               $text = $sk->makeExternalImage( htmlspecialchars( $url ) );
+                       }
+               }
+               return $text;
+       }
+
+       /**
+        * Process [[ ]] wikilinks
+        *
+        * @private
+        */
+       function replaceInternalLinks( $s ) {
+               global $wgContLang;
+               static $fname = 'Parser::replaceInternalLinks' ;
+
+               wfProfileIn( $fname );
+
+               wfProfileIn( $fname.'-setup' );
+               static $tc = FALSE;
+               # the % is needed to support urlencoded titles as well
+               if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
+
+               $sk = $this->mOptions->getSkin();
+
+               #split the entire text string on occurences of [[
+               $a = explode( '[[', ' ' . $s );
+               #get the first element (all text up to first [[), and remove the space we added
+               $s = array_shift( $a );
+               $s = substr( $s, 1 );
+
+               # Match a link having the form [[namespace:link|alternate]]trail
+               static $e1 = FALSE;
+               if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
+               # Match cases where there is no "]]", which might still be images
+               static $e1_img = FALSE;
+               if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
+               # Match the end of a line for a word that's not followed by whitespace,
+               # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
+               $e2 = wfMsgForContent( 'linkprefix' );
+
+               $useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
+               if( is_null( $this->mTitle ) ) {
+                       throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
+               }
+               $nottalk = !$this->mTitle->isTalkPage();
+
+               if ( $useLinkPrefixExtension ) {
+                       $m = array();
+                       if ( preg_match( $e2, $s, $m ) ) {
+                               $first_prefix = $m[2];
+                       } else {
+                               $first_prefix = false;
+                       }
+               } else {
+                       $prefix = '';
+               }
+
+               if($wgContLang->hasVariants()) {
+                       $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
+               } else {
+                       $selflink = array($this->mTitle->getPrefixedText());
+               }
+               $useSubpages = $this->areSubpagesAllowed();
+               wfProfileOut( $fname.'-setup' );
+
+               # Loop for each link
+               for ($k = 0; isset( $a[$k] ); $k++) {
+                       $line = $a[$k];
+                       if ( $useLinkPrefixExtension ) {
+                               wfProfileIn( $fname.'-prefixhandling' );
+                               if ( preg_match( $e2, $s, $m ) ) {
+                                       $prefix = $m[2];
+                                       $s = $m[1];
+                               } else {
+                                       $prefix='';
+                               }
+                               # first link
+                               if($first_prefix) {
+                                       $prefix = $first_prefix;
+                                       $first_prefix = false;
+                               }
+                               wfProfileOut( $fname.'-prefixhandling' );
+                       }
+
+                       $might_be_img = false;
+
+                       wfProfileIn( "$fname-e1" );
+                       if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
+                               $text = $m[2];
+                               # If we get a ] at the beginning of $m[3] that means we have a link that's something like:
+                               # [[Image:Foo.jpg|[http://example.com desc]]] <- having three ] in a row fucks up,
+                               # the real problem is with the $e1 regex
+                               # See bug 1300.
+                               #
+                               # Still some problems for cases where the ] is meant to be outside punctuation,
+                               # and no image is in sight. See bug 2095.
+                               #
+                               if( $text !== '' &&
+                                       substr( $m[3], 0, 1 ) === ']' &&
+                                       strpos($text, '[') !== false
+                               )
+                               {
+                                       $text .= ']'; # so that replaceExternalLinks($text) works later
+                                       $m[3] = substr( $m[3], 1 );
+                               }
+                               # fix up urlencoded title texts
+                               if( strpos( $m[1], '%' ) !== false ) {
+                                       # Should anchors '#' also be rejected?
+                                       $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
+                               }
+                               $trail = $m[3];
+                       } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
+                               $might_be_img = true;
+                               $text = $m[2];
+                               if ( strpos( $m[1], '%' ) !== false ) {
+                                       $m[1] = urldecode($m[1]);
+                               }
+                               $trail = "";
+                       } else { # Invalid form; output directly
+                               $s .= $prefix . '[[' . $line ;
+                               wfProfileOut( "$fname-e1" );
+                               continue;
+                       }
+                       wfProfileOut( "$fname-e1" );
+                       wfProfileIn( "$fname-misc" );
+
+                       # Don't allow internal links to pages containing
+                       # PROTO: where PROTO is a valid URL protocol; these
+                       # should be external links.
+                       if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
+                               $s .= $prefix . '[[' . $line ;
+                               continue;
+                       }
+
+                       # Make subpage if necessary
+                       if( $useSubpages ) {
+                               $link = $this->maybeDoSubpageLink( $m[1], $text );
+                       } else {
+                               $link = $m[1];
+                       }
+
+                       $noforce = (substr($m[1], 0, 1) != ':');
+                       if (!$noforce) {
+                               # Strip off leading ':'
+                               $link = substr($link, 1);
+                       }
+
+                       wfProfileOut( "$fname-misc" );
+                       wfProfileIn( "$fname-title" );
+                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
+                       if( !$nt ) {
+                               $s .= $prefix . '[[' . $line;
+                               wfProfileOut( "$fname-title" );
+                               continue;
+                       }
+
+                       $ns = $nt->getNamespace();
+                       $iw = $nt->getInterWiki();
+                       wfProfileOut( "$fname-title" );
+
+                       if ($might_be_img) { # if this is actually an invalid link
+                               wfProfileIn( "$fname-might_be_img" );
+                               if ($ns == NS_IMAGE && $noforce) { #but might be an image
+                                       $found = false;
+                                       while (isset ($a[$k+1]) ) {
+                                               #look at the next 'line' to see if we can close it there
+                                               $spliced = array_splice( $a, $k + 1, 1 );
+                                               $next_line = array_shift( $spliced );
+                                               $m = explode( ']]', $next_line, 3 );
+                                               if ( count( $m ) == 3 ) {
+                                                       # the first ]] closes the inner link, the second the image
+                                                       $found = true;
+                                                       $text .= "[[{$m[0]}]]{$m[1]}";
+                                                       $trail = $m[2];
+                                                       break;
+                                               } elseif ( count( $m ) == 2 ) {
+                                                       #if there's exactly one ]] that's fine, we'll keep looking
+                                                       $text .= "[[{$m[0]}]]{$m[1]}";
+                                               } else {
+                                                       #if $next_line is invalid too, we need look no further
+                                                       $text .= '[[' . $next_line;
+                                                       break;
+                                               }
+                                       }
+                                       if ( !$found ) {
+                                               # we couldn't find the end of this imageLink, so output it raw
+                                               #but don't ignore what might be perfectly normal links in the text we've examined
+                                               $text = $this->replaceInternalLinks($text);
+                                               $s .= "{$prefix}[[$link|$text";
+                                               # note: no $trail, because without an end, there *is* no trail
+                                               wfProfileOut( "$fname-might_be_img" );
+                                               continue;
+                                       }
+                               } else { #it's not an image, so output it raw
+                                       $s .= "{$prefix}[[$link|$text";
+                                       # note: no $trail, because without an end, there *is* no trail
+                                       wfProfileOut( "$fname-might_be_img" );
+                                       continue;
+                               }
+                               wfProfileOut( "$fname-might_be_img" );
+                       }
+
+                       $wasblank = ( '' == $text );
+                       if( $wasblank ) $text = $link;
+
+                       # Link not escaped by : , create the various objects
+                       if( $noforce ) {
+
+                               # Interwikis
+                               wfProfileIn( "$fname-interwiki" );
+                               if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+                                       $this->mOutput->addLanguageLink( $nt->getFullText() );
+                                       $s = rtrim($s . $prefix);
+                                       $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
+                                       wfProfileOut( "$fname-interwiki" );
+                                       continue;
+                               }
+                               wfProfileOut( "$fname-interwiki" );
+
+                               if ( $ns == NS_IMAGE ) {
+                                       wfProfileIn( "$fname-image" );
+                                       if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
+                                               # recursively parse links inside the image caption
+                                               # actually, this will parse them in any other parameters, too,
+                                               # but it might be hard to fix that, and it doesn't matter ATM
+                                               $text = $this->replaceExternalLinks($text);
+                                               $text = $this->replaceInternalLinks($text);
+
+                                               # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
+                                               $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text ) ) . $trail;
+                                               $this->mOutput->addImage( $nt->getDBkey() );
+
+                                               wfProfileOut( "$fname-image" );
+                                               continue;
+                                       } else {
+                                               # We still need to record the image's presence on the page
+                                               $this->mOutput->addImage( $nt->getDBkey() );
+                                       }
+                                       wfProfileOut( "$fname-image" );
+
+                               }
+
+                               if ( $ns == NS_CATEGORY ) {
+                                       wfProfileIn( "$fname-category" );
+                                       $s = rtrim($s . "\n"); # bug 87
+
+                                       if ( $wasblank ) {
+                                               $sortkey = $this->getDefaultSort();
+                                       } else {
+                                               $sortkey = $text;
+                                       }
+                                       $sortkey = Sanitizer::decodeCharReferences( $sortkey );
+                                       $sortkey = str_replace( "\n", '', $sortkey );
+                                       $sortkey = $wgContLang->convertCategoryKey( $sortkey );
+                                       $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
+
+                                       /**
+                                        * Strip the whitespace Category links produce, see bug 87
+                                        * @todo We might want to use trim($tmp, "\n") here.
+                                        */
+                                       $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+
+                                       wfProfileOut( "$fname-category" );
+                                       continue;
+                               }
+                       }
+
+                       # Self-link checking
+                       if( $nt->getFragment() === '' ) {
+                               if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
+                                       $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
+                                       continue;
+                               }
+                       }
+
+                       # Special and Media are pseudo-namespaces; no pages actually exist in them
+                       if( $ns == NS_MEDIA ) {
+                               # Give extensions a chance to select the file revision for us
+                               $skip = $time = false;
+                               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$nt, &$skip, &$time ) );
+                               if ( $skip ) {
+                                       $link = $sk->makeLinkObj( $nt );
+                               } else {
+                                       $link = $sk->makeMediaLinkObj( $nt, $text, $time );
+                               }
+                               # Cloak with NOPARSE to avoid replacement in replaceExternalLinks
+                               $s .= $prefix . $this->armorLinks( $link ) . $trail;
+                               $this->mOutput->addImage( $nt->getDBkey() );
+                               continue;
+                       } elseif( $ns == NS_SPECIAL ) {
+                               if( SpecialPage::exists( $nt->getDBkey() ) ) {
+                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+                               } else {
+                                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+                               }
+                               continue;
+                       } elseif( $ns == NS_IMAGE ) {
+                               $img = wfFindFile( $nt );
+                               if( $img ) {
+                                       // Force a blue link if the file exists; may be a remote
+                                       // upload on the shared repository, and we want to see its
+                                       // auto-generated page.
+                                       $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
+                                       $this->mOutput->addLink( $nt );
+                                       continue;
+                               }
+                       }
+                       $s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+               }
+               wfProfileOut( $fname );
+               return $s;
+       }
+
+       /**
+        * Make a link placeholder. The text returned can be later resolved to a real link with
+        * replaceLinkHolders(). This is done for two reasons: firstly to avoid further
+        * parsing of interwiki links, and secondly to allow all existence checks and
+        * article length checks (for stub links) to be bundled into a single query.
+        *
+        */
+       function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+               wfProfileIn( __METHOD__ );
+               if ( ! is_object($nt) ) {
+                       # Fail gracefully
+                       $retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
+               } else {
+                       # Separate the link trail from the rest of the link
+                       list( $inside, $trail ) = Linker::splitTrail( $trail );
+
+                       if ( $nt->isExternal() ) {
+                               $nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
+                               $this->mInterwikiLinkHolders['titles'][] = $nt;
+                               $retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
+                       } else {
+                               $nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
+                               $this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
+                               $this->mLinkHolders['queries'][] = $query;
+                               $this->mLinkHolders['texts'][] = $prefix.$text.$inside;
+                               $this->mLinkHolders['titles'][] = $nt;
+
+                               $retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               return $retVal;
+       }
+
+       /**
+        * Render a forced-blue link inline; protect against double expansion of
+        * URLs if we're in a mode that prepends full URL prefixes to internal links.
+        * Since this little disaster has to split off the trail text to avoid
+        * breaking URLs in the following text without breaking trails on the
+        * wiki links, it's been made into a horrible function.
+        *
+        * @param Title $nt
+        * @param string $text
+        * @param string $query
+        * @param string $trail
+        * @param string $prefix
+        * @return string HTML-wikitext mix oh yuck
+        */
+       function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+               list( $inside, $trail ) = Linker::splitTrail( $trail );
+               $sk = $this->mOptions->getSkin();
+               $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
+               return $this->armorLinks( $link ) . $trail;
+       }
+
+       /**
+        * Insert a NOPARSE hacky thing into any inline links in a chunk that's
+        * going to go through further parsing steps before inline URL expansion.
+        *
+        * In particular this is important when using action=render, which causes
+        * full URLs to be included.
+        *
+        * Oh man I hate our multi-layer parser!
+        *
+        * @param string more-or-less HTML
+        * @return string less-or-more HTML with NOPARSE bits
+        */
+       function armorLinks( $text ) {
+               return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
+                       "{$this->mUniqPrefix}NOPARSE$1", $text );
+       }
+
+       /**
+        * Return true if subpage links should be expanded on this page.
+        * @return bool
+        */
+       function areSubpagesAllowed() {
+               # Some namespaces don't allow subpages
+               global $wgNamespacesWithSubpages;
+               return !empty($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]);
+       }
+
+       /**
+        * Handle link to subpage if necessary
+        * @param string $target the source of the link
+        * @param string &$text the link text, modified as necessary
+        * @return string the full name of the link
+        * @private
+        */
+       function maybeDoSubpageLink($target, &$text) {
+               # Valid link forms:
+               # Foobar -- normal
+               # :Foobar -- override special treatment of prefix (images, language links)
+               # /Foobar -- convert to CurrentPage/Foobar
+               # /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
+               # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
+               # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
+
+               $fname = 'Parser::maybeDoSubpageLink';
+               wfProfileIn( $fname );
+               $ret = $target; # default return value is no change
+
+               # Some namespaces don't allow subpages,
+               # so only perform processing if subpages are allowed
+               if( $this->areSubpagesAllowed() ) {
+                       $hash = strpos( $target, '#' );
+                       if( $hash !== false ) {
+                               $suffix = substr( $target, $hash );
+                               $target = substr( $target, 0, $hash );
+                       } else {
+                               $suffix = '';
+                       }
+                       # bug 7425
+                       $target = trim( $target );
+                       # Look at the first character
+                       if( $target != '' && $target{0} == '/' ) {
+                               # / at end means we don't want the slash to be shown
+                               $m = array();
+                               $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
+                               if( $trailingSlashes ) {
+                                       $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
+                               } else {
+                                       $noslash = substr( $target, 1 );
+                               }
+
+                               $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
+                               if( '' === $text ) {
+                                       $text = $target . $suffix;
+                               } # this might be changed for ugliness reasons
+                       } else {
+                               # check for .. subpage backlinks
+                               $dotdotcount = 0;
+                               $nodotdot = $target;
+                               while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
+                                       ++$dotdotcount;
+                                       $nodotdot = substr( $nodotdot, 3 );
+                               }
+                               if($dotdotcount > 0) {
+                                       $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
+                                       if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
+                                               $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
+                                               # / at the end means don't show full path
+                                               if( substr( $nodotdot, -1, 1 ) == '/' ) {
+                                                       $nodotdot = substr( $nodotdot, 0, -1 );
+                                                       if( '' === $text ) {
+                                                               $text = $nodotdot . $suffix;
+                                                       }
+                                               }
+                                               $nodotdot = trim( $nodotdot );
+                                               if( $nodotdot != '' ) {
+                                                       $ret .= '/' . $nodotdot;
+                                               }
+                                               $ret .= $suffix;
+                                       }
+                               }
+                       }
+               }
+
+               wfProfileOut( $fname );
+               return $ret;
+       }
+
+       /**#@+
+        * Used by doBlockLevels()
+        * @private
+        */
+       /* private */ function closeParagraph() {
+               $result = '';
+               if ( '' != $this->mLastSection ) {
+                       $result = '</' . $this->mLastSection  . ">\n";
+               }
+               $this->mInPre = false;
+               $this->mLastSection = '';
+               return $result;
+       }
+       # getCommon() returns the length of the longest common substring
+       # of both arguments, starting at the beginning of both.
+       #
+       /* private */ function getCommon( $st1, $st2 ) {
+               $fl = strlen( $st1 );
+               $shorter = strlen( $st2 );
+               if ( $fl < $shorter ) { $shorter = $fl; }
+
+               for ( $i = 0; $i < $shorter; ++$i ) {
+                       if ( $st1{$i} != $st2{$i} ) { break; }
+               }
+               return $i;
+       }
+       # These next three functions open, continue, and close the list
+       # element appropriate to the prefix character passed into them.
+       #
+       /* private */ function openList( $char ) {
+               $result = $this->closeParagraph();
+
+               if ( '*' == $char ) { $result .= '<ul><li>'; }
+               else if ( '#' == $char ) { $result .= '<ol><li>'; }
+               else if ( ':' == $char ) { $result .= '<dl><dd>'; }
+               else if ( ';' == $char ) {
+                       $result .= '<dl><dt>';
+                       $this->mDTopen = true;
+               }
+               else { $result = '<!-- ERR 1 -->'; }
+
+               return $result;
+       }
+
+       /* private */ function nextItem( $char ) {
+               if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
+               else if ( ':' == $char || ';' == $char ) {
+                       $close = '</dd>';
+                       if ( $this->mDTopen ) { $close = '</dt>'; }
+                       if ( ';' == $char ) {
+                               $this->mDTopen = true;
+                               return $close . '<dt>';
+                       } else {
+                               $this->mDTopen = false;
+                               return $close . '<dd>';
+                       }
+               }
+               return '<!-- ERR 2 -->';
+       }
+
+       /* private */ function closeList( $char ) {
+               if ( '*' == $char ) { $text = '</li></ul>'; }
+               else if ( '#' == $char ) { $text = '</li></ol>'; }
+               else if ( ':' == $char ) {
+                       if ( $this->mDTopen ) {
+                               $this->mDTopen = false;
+                               $text = '</dt></dl>';
+                       } else {
+                               $text = '</dd></dl>';
+                       }
+               }
+               else {  return '<!-- ERR 3 -->'; }
+               return $text."\n";
+       }
+       /**#@-*/
+
+       /**
+        * Make lists from lines starting with ':', '*', '#', etc.
+        *
+        * @private
+        * @return string the lists rendered as HTML
+        */
+       function doBlockLevels( $text, $linestart ) {
+               $fname = 'Parser::doBlockLevels';
+               wfProfileIn( $fname );
+
+               # Parsing through the text line by line.  The main thing
+               # happening here is handling of block-level elements p, pre,
+               # and making lists from lines starting with * # : etc.
+               #
+               $textLines = explode( "\n", $text );
+
+               $lastPrefix = $output = '';
+               $this->mDTopen = $inBlockElem = false;
+               $prefixLength = 0;
+               $paragraphStack = false;
+
+               if ( !$linestart ) {
+                       $output .= array_shift( $textLines );
+               }
+               foreach ( $textLines as $oLine ) {
+                       $lastPrefixLength = strlen( $lastPrefix );
+                       $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
+                       $preOpenMatch = preg_match('/<pre/i', $oLine );
+                       if ( !$this->mInPre ) {
+                               # Multiple prefixes may abut each other for nested lists.
+                               $prefixLength = strspn( $oLine, '*#:;' );
+                               $pref = substr( $oLine, 0, $prefixLength );
+
+                               # eh?
+                               $pref2 = str_replace( ';', ':', $pref );
+                               $t = substr( $oLine, $prefixLength );
+                               $this->mInPre = !empty($preOpenMatch);
+                       } else {
+                               # Don't interpret any other prefixes in preformatted text
+                               $prefixLength = 0;
+                               $pref = $pref2 = '';
+                               $t = $oLine;
+                       }
+
+                       # List generation
+                       if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
+                               # Same as the last item, so no need to deal with nesting or opening stuff
+                               $output .= $this->nextItem( substr( $pref, -1 ) );
+                               $paragraphStack = false;
+
+                               if ( substr( $pref, -1 ) == ';') {
+                                       # The one nasty exception: definition lists work like this:
+                                       # ; title : definition text
+                                       # So we check for : in the remainder text to split up the
+                                       # title and definition, without b0rking links.
+                                       $term = $t2 = '';
+                                       if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+                                               $t = $t2;
+                                               $output .= $term . $this->nextItem( ':' );
+                                       }
+                               }
+                       } elseif( $prefixLength || $lastPrefixLength ) {
+                               # Either open or close a level...
+                               $commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
+                               $paragraphStack = false;
+
+                               while( $commonPrefixLength < $lastPrefixLength ) {
+                                       $output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
+                                       --$lastPrefixLength;
+                               }
+                               if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
+                                       $output .= $this->nextItem( $pref{$commonPrefixLength-1} );
+                               }
+                               while ( $prefixLength > $commonPrefixLength ) {
+                                       $char = substr( $pref, $commonPrefixLength, 1 );
+                                       $output .= $this->openList( $char );
+
+                                       if ( ';' == $char ) {
+                                               # FIXME: This is dupe of code above
+                                               if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+                                                       $t = $t2;
+                                                       $output .= $term . $this->nextItem( ':' );
+                                               }
+                                       }
+                                       ++$commonPrefixLength;
+                               }
+                               $lastPrefix = $pref2;
+                       }
+                       if( 0 == $prefixLength ) {
+                               wfProfileIn( "$fname-paragraph" );
+                               # No prefix (not in list)--go to paragraph mode
+                               // XXX: use a stack for nestable elements like span, table and div
+                               $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
+                               $closematch = preg_match(
+                                       '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
+                                       '<td|<th|<\\/?div|<hr|<\\/pre|<\\/p|'.$this->mUniqPrefix.'-pre|<\\/li|<\\/ul|<\\/ol|<\\/?center)/iS', $t );
+                               if ( $openmatch or $closematch ) {
+                                       $paragraphStack = false;
+                                       # TODO bug 5718: paragraph closed
+                                       $output .= $this->closeParagraph();
+                                       if ( $preOpenMatch and !$preCloseMatch ) {
+                                               $this->mInPre = true;
+                                       }
+                                       if ( $closematch ) {
+                                               $inBlockElem = false;
+                                       } else {
+                                               $inBlockElem = true;
+                                       }
+                               } else if ( !$inBlockElem && !$this->mInPre ) {
+                                       if ( '' != $t and ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
+                                               // pre
+                                               if ($this->mLastSection != 'pre') {
+                                                       $paragraphStack = false;
+                                                       $output .= $this->closeParagraph().'<pre>';
+                                                       $this->mLastSection = 'pre';
+                                               }
+                                               $t = substr( $t, 1 );
+                                       } else {
+                                               // paragraph
+                                               if ( '' == trim($t) ) {
+                                                       if ( $paragraphStack ) {
+                                                               $output .= $paragraphStack.'<br />';
+                                                               $paragraphStack = false;
+                                                               $this->mLastSection = 'p';
+                                                       } else {
+                                                               if ($this->mLastSection != 'p' ) {
+                                                                       $output .= $this->closeParagraph();
+                                                                       $this->mLastSection = '';
+                                                                       $paragraphStack = '<p>';
+                                                               } else {
+                                                                       $paragraphStack = '</p><p>';
+                                                               }
+                                                       }
+                                               } else {
+                                                       if ( $paragraphStack ) {
+                                                               $output .= $paragraphStack;
+                                                               $paragraphStack = false;
+                                                               $this->mLastSection = 'p';
+                                                       } else if ($this->mLastSection != 'p') {
+                                                               $output .= $this->closeParagraph().'<p>';
+                                                               $this->mLastSection = 'p';
+                                                       }
+                                               }
+                                       }
+                               }
+                               wfProfileOut( "$fname-paragraph" );
+                       }
+                       // somewhere above we forget to get out of pre block (bug 785)
+                       if($preCloseMatch && $this->mInPre) {
+                               $this->mInPre = false;
+                       }
+                       if ($paragraphStack === false) {
+                               $output .= $t."\n";
+                       }
+               }
+               while ( $prefixLength ) {
+                       $output .= $this->closeList( $pref2{$prefixLength-1} );
+                       --$prefixLength;
+               }
+               if ( '' != $this->mLastSection ) {
+                       $output .= '</' . $this->mLastSection . '>';
+                       $this->mLastSection = '';
+               }
+
+               wfProfileOut( $fname );
+               return $output;
+       }
+
+       /**
+        * Split up a string on ':', ignoring any occurences inside tags
+        * to prevent illegal overlapping.
+        * @param string $str the string to split
+        * @param string &$before set to everything before the ':'
+        * @param string &$after set to everything after the ':'
+        * return string the position of the ':', or false if none found
+        */
+       function findColonNoLinks($str, &$before, &$after) {
+               $fname = 'Parser::findColonNoLinks';
+               wfProfileIn( $fname );
+
+               $pos = strpos( $str, ':' );
+               if( $pos === false ) {
+                       // Nothing to find!
+                       wfProfileOut( $fname );
+                       return false;
+               }
+
+               $lt = strpos( $str, '<' );
+               if( $lt === false || $lt > $pos ) {
+                       // Easy; no tag nesting to worry about
+                       $before = substr( $str, 0, $pos );
+                       $after = substr( $str, $pos+1 );
+                       wfProfileOut( $fname );
+                       return $pos;
+               }
+
+               // Ugly state machine to walk through avoiding tags.
+               $state = self::COLON_STATE_TEXT;
+               $stack = 0;
+               $len = strlen( $str );
+               for( $i = 0; $i < $len; $i++ ) {
+                       $c = $str{$i};
+
+                       switch( $state ) {
+                       // (Using the number is a performance hack for common cases)
+                       case 0: // self::COLON_STATE_TEXT:
+                               switch( $c ) {
+                               case "<":
+                                       // Could be either a <start> tag or an </end> tag
+                                       $state = self::COLON_STATE_TAGSTART;
+                                       break;
+                               case ":":
+                                       if( $stack == 0 ) {
+                                               // We found it!
+                                               $before = substr( $str, 0, $i );
+                                               $after = substr( $str, $i + 1 );
+                                               wfProfileOut( $fname );
+                                               return $i;
+                                       }
+                                       // Embedded in a tag; don't break it.
+                                       break;
+                               default:
+                                       // Skip ahead looking for something interesting
+                                       $colon = strpos( $str, ':', $i );
+                                       if( $colon === false ) {
+                                               // Nothing else interesting
+                                               wfProfileOut( $fname );
+                                               return false;
+                                       }
+                                       $lt = strpos( $str, '<', $i );
+                                       if( $stack === 0 ) {
+                                               if( $lt === false || $colon < $lt ) {
+                                                       // We found it!
+                                                       $before = substr( $str, 0, $colon );
+                                                       $after = substr( $str, $colon + 1 );
+                                                       wfProfileOut( $fname );
+                                                       return $i;
+                                               }
+                                       }
+                                       if( $lt === false ) {
+                                               // Nothing else interesting to find; abort!
+                                               // We're nested, but there's no close tags left. Abort!
+                                               break 2;
+                                       }
+                                       // Skip ahead to next tag start
+                                       $i = $lt;
+                                       $state = self::COLON_STATE_TAGSTART;
+                               }
+                               break;
+                       case 1: // self::COLON_STATE_TAG:
+                               // In a <tag>
+                               switch( $c ) {
+                               case ">":
+                                       $stack++;
+                                       $state = self::COLON_STATE_TEXT;
+                                       break;
+                               case "/":
+                                       // Slash may be followed by >?
+                                       $state = self::COLON_STATE_TAGSLASH;
+                                       break;
+                               default:
+                                       // ignore
+                               }
+                               break;
+                       case 2: // self::COLON_STATE_TAGSTART:
+                               switch( $c ) {
+                               case "/":
+                                       $state = self::COLON_STATE_CLOSETAG;
+                                       break;
+                               case "!":
+                                       $state = self::COLON_STATE_COMMENT;
+                                       break;
+                               case ">":
+                                       // Illegal early close? This shouldn't happen D:
+                                       $state = self::COLON_STATE_TEXT;
+                                       break;
+                               default:
+                                       $state = self::COLON_STATE_TAG;
+                               }
+                               break;
+                       case 3: // self::COLON_STATE_CLOSETAG:
+                               // In a </tag>
+                               if( $c == ">" ) {
+                                       $stack--;
+                                       if( $stack < 0 ) {
+                                               wfDebug( "Invalid input in $fname; too many close tags\n" );
+                                               wfProfileOut( $fname );
+                                               return false;
+                                       }
+                                       $state = self::COLON_STATE_TEXT;
+                               }
+                               break;
+                       case self::COLON_STATE_TAGSLASH:
+                               if( $c == ">" ) {
+                                       // Yes, a self-closed tag <blah/>
+                                       $state = self::COLON_STATE_TEXT;
+                               } else {
+                                       // Probably we're jumping the gun, and this is an attribute
+                                       $state = self::COLON_STATE_TAG;
+                               }
+                               break;
+                       case 5: // self::COLON_STATE_COMMENT:
+                               if( $c == "-" ) {
+                                       $state = self::COLON_STATE_COMMENTDASH;
+                               }
+                               break;
+                       case self::COLON_STATE_COMMENTDASH:
+                               if( $c == "-" ) {
+                                       $state = self::COLON_STATE_COMMENTDASHDASH;
+                               } else {
+                                       $state = self::COLON_STATE_COMMENT;
+                               }
+                               break;
+                       case self::COLON_STATE_COMMENTDASHDASH:
+                               if( $c == ">" ) {
+                                       $state = self::COLON_STATE_TEXT;
+                               } else {
+                                       $state = self::COLON_STATE_COMMENT;
+                               }
+                               break;
+                       default:
+                               throw new MWException( "State machine error in $fname" );
+                       }
+               }
+               if( $stack > 0 ) {
+                       wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+                       return false;
+               }
+               wfProfileOut( $fname );
+               return false;
+       }
+
+       /**
+        * Return value of a magic variable (like PAGENAME)
+        *
+        * @private
+        */
+       function getVariableValue( $index ) {
+               global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
+
+               /**
+                * Some of these require message or data lookups and can be
+                * expensive to check many times.
+                */
+               static $varCache = array();
+               if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$varCache ) ) ) {
+                       if ( isset( $varCache[$index] ) ) {
+                               return $varCache[$index];
+                       }
+               }
+
+               $ts = time();
+               wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
+
+               # Use the time zone
+               global $wgLocaltimezone;
+               if ( isset( $wgLocaltimezone ) ) {
+                       $oldtz = getenv( 'TZ' );
+                       putenv( 'TZ='.$wgLocaltimezone );
+               }
+
+               wfSuppressWarnings(); // E_STRICT system time bitching
+               $localTimestamp = date( 'YmdHis', $ts );
+               $localMonth = date( 'm', $ts );
+               $localMonthName = date( 'n', $ts );
+               $localDay = date( 'j', $ts );
+               $localDay2 = date( 'd', $ts );
+               $localDayOfWeek = date( 'w', $ts );
+               $localWeek = date( 'W', $ts );
+               $localYear = date( 'Y', $ts );
+               $localHour = date( 'H', $ts );
+               if ( isset( $wgLocaltimezone ) ) {
+                       putenv( 'TZ='.$oldtz );
+               }
+               wfRestoreWarnings();
+
+               switch ( $index ) {
+                       case 'currentmonth':
+                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+                       case 'currentmonthname':
+                               return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+                       case 'currentmonthnamegen':
+                               return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+                       case 'currentmonthabbrev':
+                               return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+                       case 'currentday':
+                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+                       case 'currentday2':
+                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+                       case 'localmonth':
+                               return $varCache[$index] = $wgContLang->formatNum( $localMonth );
+                       case 'localmonthname':
+                               return $varCache[$index] = $wgContLang->getMonthName( $localMonthName );
+                       case 'localmonthnamegen':
+                               return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
+                       case 'localmonthabbrev':
+                               return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
+                       case 'localday':
+                               return $varCache[$index] = $wgContLang->formatNum( $localDay );
+                       case 'localday2':
+                               return $varCache[$index] = $wgContLang->formatNum( $localDay2 );
+                       case 'pagename':
+                               return wfEscapeWikiText( $this->mTitle->getText() );
+                       case 'pagenamee':
+                               return $this->mTitle->getPartialURL();
+                       case 'fullpagename':
+                               return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+                       case 'fullpagenamee':
+                               return $this->mTitle->getPrefixedURL();
+                       case 'subpagename':
+                               return wfEscapeWikiText( $this->mTitle->getSubpageText() );
+                       case 'subpagenamee':
+                               return $this->mTitle->getSubpageUrlForm();
+                       case 'basepagename':
+                               return wfEscapeWikiText( $this->mTitle->getBaseText() );
+                       case 'basepagenamee':
+                               return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+                       case 'talkpagename':
+                               if( $this->mTitle->canTalk() ) {
+                                       $talkPage = $this->mTitle->getTalkPage();
+                                       return wfEscapeWikiText( $talkPage->getPrefixedText() );
+                               } else {
+                                       return '';
+                               }
+                       case 'talkpagenamee':
+                               if( $this->mTitle->canTalk() ) {
+                                       $talkPage = $this->mTitle->getTalkPage();
+                                       return $talkPage->getPrefixedUrl();
+                               } else {
+                                       return '';
+                               }
+                       case 'subjectpagename':
+                               $subjPage = $this->mTitle->getSubjectPage();
+                               return wfEscapeWikiText( $subjPage->getPrefixedText() );
+                       case 'subjectpagenamee':
+                               $subjPage = $this->mTitle->getSubjectPage();
+                               return $subjPage->getPrefixedUrl();
+                       case 'revisionid':
+                               return $this->mRevisionId;
+                       case 'revisionday':
+                               return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+                       case 'revisionday2':
+                               return substr( $this->getRevisionTimestamp(), 6, 2 );
+                       case 'revisionmonth':
+                               return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+                       case 'revisionyear':
+                               return substr( $this->getRevisionTimestamp(), 0, 4 );
+                       case 'revisiontimestamp':
+                               return $this->getRevisionTimestamp();
+                       case 'namespace':
+                               return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                       case 'namespacee':
+                               return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                       case 'talkspace':
+                               return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
+                       case 'talkspacee':
+                               return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+                       case 'subjectspace':
+                               return $this->mTitle->getSubjectNsText();
+                       case 'subjectspacee':
+                               return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+                       case 'currentdayname':
+                               return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+                       case 'currentyear':
+                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+                       case 'currenttime':
+                               return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+                       case 'currenthour':
+                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+                       case 'currentweek':
+                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+                               // int to remove the padding
+                               return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+                       case 'currentdow':
+                               return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+                       case 'localdayname':
+                               return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+                       case 'localyear':
+                               return $varCache[$index] = $wgContLang->formatNum( $localYear, true );
+                       case 'localtime':
+                               return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false );
+                       case 'localhour':
+                               return $varCache[$index] = $wgContLang->formatNum( $localHour, true );
+                       case 'localweek':
+                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+                               // int to remove the padding
+                               return $varCache[$index] = $wgContLang->formatNum( (int)$localWeek );
+                       case 'localdow':
+                               return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
+                       case 'numberofarticles':
+                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
+                       case 'numberoffiles':
+                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() );
+                       case 'numberofusers':
+                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+                       case 'numberofpages':
+                               return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
+                       case 'numberofadmins':
+                               return $varCache[$index]  = $wgContLang->formatNum( SiteStats::admins() );
+                       case 'numberofedits':
+                               return $varCache[$index]  = $wgContLang->formatNum( SiteStats::edits() );
+                       case 'currenttimestamp':
+                               return $varCache[$index] = wfTimestampNow();
+                       case 'localtimestamp':
+                               return $varCache[$index] = $localTimestamp;
+                       case 'currentversion':
+                               return $varCache[$index] = SpecialVersion::getVersion();
+                       case 'sitename':
+                               return $wgSitename;
+                       case 'server':
+                               return $wgServer;
+                       case 'servername':
+                               return $wgServerName;
+                       case 'scriptpath':
+                               return $wgScriptPath;
+                       case 'directionmark':
+                               return $wgContLang->getDirMark();
+                       case 'contentlanguage':
+                               global $wgContLanguageCode;
+                               return $wgContLanguageCode;
+                       default:
+                               $ret = null;
+                               if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
+                                       return $ret;
+                               else
+                                       return null;
+               }
+       }
+
+       /**
+        * initialise the magic variables (like CURRENTMONTHNAME)
+        *
+        * @private
+        */
+       function initialiseVariables() {
+               $fname = 'Parser::initialiseVariables';
+               wfProfileIn( $fname );
+               $variableIDs = MagicWord::getVariableIDs();
+
+               $this->mVariables = array();
+               foreach ( $variableIDs as $id ) {
+                       $mw =& MagicWord::get( $id );
+                       $mw->addToArray( $this->mVariables, $id );
+               }
+               wfProfileOut( $fname );
+       }
+
+       /**
+        * parse any parentheses in format ((title|part|part))
+        * and call callbacks to get a replacement text for any found piece
+        *
+        * @param string $text The text to parse
+        * @param array $callbacks rules in form:
+        *     '{' => array(                            # opening parentheses
+        *                                      'end' => '}',   # closing parentheses
+        *                                      'cb' => array(2 => callback,    # replacement callback to call if {{..}} is found
+        *                                                                3 => callback         # replacement callback to call if {{{..}}} is found
+        *                                                                )
+        *                                      )
+        *                                      'min' => 2,     # Minimum parenthesis count in cb
+        *                                      'max' => 3,     # Maximum parenthesis count in cb
+        * @private
+        */
+       function replace_callback ($text, $callbacks) {
+               wfProfileIn( __METHOD__ );
+               $openingBraceStack = array();   # this array will hold a stack of parentheses which are not closed yet
+               $lastOpeningBrace = -1;                 # last not closed parentheses
+
+               $validOpeningBraces = implode( '', array_keys( $callbacks ) );
+
+               $i = 0;
+               while ( $i < strlen( $text ) ) {
+                       # Find next opening brace, closing brace or pipe
+                       if ( $lastOpeningBrace == -1 ) {
+                               $currentClosing = '';
+                               $search = $validOpeningBraces;
+                       } else {
+                               $currentClosing = $openingBraceStack[$lastOpeningBrace]['braceEnd'];
+                               $search = $validOpeningBraces . '|' . $currentClosing;
+                       }
+                       $rule = null;
+                       $i += strcspn( $text, $search, $i );
+                       if ( $i < strlen( $text ) ) {
+                               if ( $text[$i] == '|' ) {
+                                       $found = 'pipe';
+                               } elseif ( $text[$i] == $currentClosing ) {
+                                       $found = 'close';
+                               } elseif ( isset( $callbacks[$text[$i]] ) ) {
+                                       $found = 'open';
+                                       $rule = $callbacks[$text[$i]];
+                               } else {
+                                       # Some versions of PHP have a strcspn which stops on null characters
+                                       # Ignore and continue
+                                       ++$i;
+                                       continue;
+                               }
+                       } else {
+                               # All done
+                               break;
+                       }
+
+                       if ( $found == 'open' ) {
+                               # found opening brace, let's add it to parentheses stack
+                               $piece = array('brace' => $text[$i],
+                                                          'braceEnd' => $rule['end'],
+                                                          'title' => '',
+                                                          'parts' => null);
+
+                               # count opening brace characters
+                               $piece['count'] = strspn( $text, $piece['brace'], $i );
+                               $piece['startAt'] = $piece['partStart'] = $i + $piece['count'];
+                               $i += $piece['count'];
+
+                               # we need to add to stack only if opening brace count is enough for one of the rules
+                               if ( $piece['count'] >= $rule['min'] ) {
+                                       $lastOpeningBrace ++;
+                                       $openingBraceStack[$lastOpeningBrace] = $piece;
+                               }
+                       } elseif ( $found == 'close' ) {
+                               # lets check if it is enough characters for closing brace
+                               $maxCount = $openingBraceStack[$lastOpeningBrace]['count'];
+                               $count = strspn( $text, $text[$i], $i, $maxCount );
+
+                               # check for maximum matching characters (if there are 5 closing
+                               # characters, we will probably need only 3 - depending on the rules)
+                               $matchingCount = 0;
+                               $matchingCallback = null;
+                               $cbType = $callbacks[$openingBraceStack[$lastOpeningBrace]['brace']];
+                               if ( $count > $cbType['max'] ) {
+                                       # The specified maximum exists in the callback array, unless the caller
+                                       # has made an error
+                                       $matchingCount = $cbType['max'];
+                               } else {
+                                       # Count is less than the maximum
+                                       # Skip any gaps in the callback array to find the true largest match
+                                       # Need to use array_key_exists not isset because the callback can be null
+                                       $matchingCount = $count;
+                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $cbType['cb'] ) ) {
+                                               --$matchingCount;
+                                       }
+                               }
+
+                               if ($matchingCount <= 0) {
+                                       $i += $count;
+                                       continue;
+                               }
+                               $matchingCallback = $cbType['cb'][$matchingCount];
+
+                               # let's set a title or last part (if '|' was found)
+                               if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
+                                       $openingBraceStack[$lastOpeningBrace]['title'] =
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                               } else {
+                                       $openingBraceStack[$lastOpeningBrace]['parts'][] =
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                               }
+
+                               $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
+                               $pieceEnd = $i + $matchingCount;
+
+                               if( is_callable( $matchingCallback ) ) {
+                                       $cbArgs = array (
+                                                                        'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
+                                                                        'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
+                                                                        'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
+                                                                        'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == "\n")),
+                                                                        );
+                                       # finally we can call a user callback and replace piece of text
+                                       $replaceWith = call_user_func( $matchingCallback, $cbArgs );
+                                       $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
+                                       $i = $pieceStart + strlen($replaceWith);
+                               } else {
+                                       # null value for callback means that parentheses should be parsed, but not replaced
+                                       $i += $matchingCount;
+                               }
+
+                               # reset last opening parentheses, but keep it in case there are unused characters
+                               $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
+                                                          'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
+                                                          'count' => $openingBraceStack[$lastOpeningBrace]['count'],
+                                                          'title' => '',
+                                                          'parts' => null,
+                                                          'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
+                               $openingBraceStack[$lastOpeningBrace--] = null;
+
+                               if ($matchingCount < $piece['count']) {
+                                       $piece['count'] -= $matchingCount;
+                                       $piece['startAt'] -= $matchingCount;
+                                       $piece['partStart'] = $piece['startAt'];
+                                       # do we still qualify for any callback with remaining count?
+                                       $currentCbList = $callbacks[$piece['brace']]['cb'];
+                                       while ( $piece['count'] ) {
+                                               if ( array_key_exists( $piece['count'], $currentCbList ) ) {
+                                                       $lastOpeningBrace++;
+                                                       $openingBraceStack[$lastOpeningBrace] = $piece;
+                                                       break;
+                                               }
+                                               --$piece['count'];
+                                       }
+                               }
+                       } elseif ( $found == 'pipe' ) {
+                               # lets set a title if it is a first separator, or next part otherwise
+                               if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
+                                       $openingBraceStack[$lastOpeningBrace]['title'] =
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                                       $openingBraceStack[$lastOpeningBrace]['parts'] = array();
+                               } else {
+                                       $openingBraceStack[$lastOpeningBrace]['parts'][] =
+                                               substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'],
+                                               $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+                               }
+                               $openingBraceStack[$lastOpeningBrace]['partStart'] = ++$i;
+                       }
+               }
+
+               wfProfileOut( __METHOD__ );
+               return $text;
+       }
+
+       /**
+        * Replace magic variables, templates, and template arguments
+        * with the appropriate text. Templates are substituted recursively,
+        * taking care to avoid infinite loops.
+        *
+        * Note that the substitution depends on value of $mOutputType:
+        *  self::OT_WIKI: only {{subst:}} templates
+        *  self::OT_MSG: only magic variables
+        *  self::OT_HTML: all templates and magic variables
+        *
+        * @param string $tex The text to transform
+        * @param array $args Key-value pairs representing template parameters to substitute
+        * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
+        * @private
+        */
+       function replaceVariables( $text, $args = array(), $argsOnly = false ) {
+               # Prevent too big inclusions
+               if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
+                       return $text;
+               }
+
+               $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
+               wfProfileIn( $fname );
+
+               # This function is called recursively. To keep track of arguments we need a stack:
+               array_push( $this->mArgStack, $args );
+
+               $braceCallbacks = array();
+               if ( !$argsOnly ) {
+                       $braceCallbacks[2] = array( &$this, 'braceSubstitution' );
+               }
+               if ( $this->mOutputType != self::OT_MSG ) {
+                       $braceCallbacks[3] = array( &$this, 'argSubstitution' );
+               }
+               if ( $braceCallbacks ) {
+                       $callbacks = array(
+                               '{' => array(
+                                       'end' => '}',
+                                       'cb' => $braceCallbacks,
+                                       'min' => $argsOnly ? 3 : 2,
+                                       'max' => isset( $braceCallbacks[3] ) ? 3 : 2,
+                               ),
+                               '[' => array(
+                                       'end' => ']',
+                                       'cb' => array(2=>null),
+                                       'min' => 2,
+                                       'max' => 2,
+                               )
+                       );
+                       $text = $this->replace_callback ($text, $callbacks);
+
+                       array_pop( $this->mArgStack );
+               }
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * Replace magic variables
+        * @private
+        */
+       function variableSubstitution( $matches ) {
+               global $wgContLang;
+               $fname = 'Parser::variableSubstitution';
+               $varname = $wgContLang->lc($matches[1]);
+               wfProfileIn( $fname );
+               $skip = false;
+               if ( $this->mOutputType == self::OT_WIKI ) {
+                       # Do only magic variables prefixed by SUBST
+                       $mwSubst =& MagicWord::get( 'subst' );
+                       if (!$mwSubst->matchStartAndRemove( $varname ))
+                               $skip = true;
+                       # Note that if we don't substitute the variable below,
+                       # we don't remove the {{subst:}} magic word, in case
+                       # it is a template rather than a magic variable.
+               }
+               if ( !$skip && array_key_exists( $varname, $this->mVariables ) ) {
+                       $id = $this->mVariables[$varname];
+                       # Now check if we did really match, case sensitive or not
+                       $mw =& MagicWord::get( $id );
+                       if ($mw->match($matches[1])) {
+                               $text = $this->getVariableValue( $id );
+                               if (MagicWord::getCacheTTL($id)>-1)
+                                       $this->mOutput->mContainsOldMagic = true;
+                       } else {
+                               $text = $matches[0];
+                       }
+               } else {
+                       $text = $matches[0];
+               }
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+
+       /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
+       static function createAssocArgs( $args ) {
+               $assocArgs = array();
+               $index = 1;
+               foreach( $args as $arg ) {
+                       $eqpos = strpos( $arg, '=' );
+                       if ( $eqpos === false ) {
+                               $assocArgs[$index++] = $arg;
+                       } else {
+                               $name = trim( substr( $arg, 0, $eqpos ) );
+                               $value = trim( substr( $arg, $eqpos+1 ) );
+                               if ( $value === false ) {
+                                       $value = '';
+                               }
+                               if ( $name !== false ) {
+                                       $assocArgs[$name] = $value;
+                               }
+                       }
+               }
+
+               return $assocArgs;
+       }
+
+       /**
+        * Return the text of a template, after recursively
+        * replacing any variables or templates within the template.
+        *
+        * @param array $piece The parts of the template
+        *  $piece['text']: matched text
+        *  $piece['title']: the title, i.e. the part before the |
+        *  $piece['parts']: the parameter array
+        * @return string the text of the template
+        * @private
+        */
+       function braceSubstitution( $piece ) {
+               global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
+               $fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
+               wfProfileIn( $fname );
+               wfProfileIn( __METHOD__.'-setup' );
+
+               # Flags
+               $found = false;             # $text has been filled
+               $nowiki = false;            # wiki markup in $text should be escaped
+               $noparse = false;           # Unsafe HTML tags should not be stripped, etc.
+               $noargs = false;            # Don't replace triple-brace arguments in $text
+               $replaceHeadings = false;   # Make the edit section links go to the template not the article
+                $headingOffset = 0;         # Skip headings when number, to account for those that weren't transcluded.
+               $isHTML = false;            # $text is HTML, armour it against wikitext transformation
+               $forceRawInterwiki = false; # Force interwiki transclusion to be done in raw mode not rendered
+
+               # Title object, where $text came from
+               $title = NULL;
+
+               $linestart = '';
+
+
+               # $part1 is the bit before the first |, and must contain only title characters
+               # $args is a list of arguments, starting from index 0, not including $part1
+
+               $titleText = $part1 = $piece['title'];
+               # If the third subpattern matched anything, it will start with |
+
+               if (null == $piece['parts']) {
+                       $replaceWith = $this->variableSubstitution (array ($piece['text'], $piece['title']));
+                       if ($replaceWith != $piece['text']) {
+                               $text = $replaceWith;
+                               $found = true;
+                               $noparse = true;
+                               $noargs = true;
+                       }
+               }
+
+               $args = (null == $piece['parts']) ? array() : $piece['parts'];
+               wfProfileOut( __METHOD__.'-setup' );
+
+               # SUBST
+               wfProfileIn( __METHOD__.'-modifiers' );
+               if ( !$found ) {
+                       $mwSubst =& MagicWord::get( 'subst' );
+                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
+                               # One of two possibilities is true:
+                               # 1) Found SUBST but not in the PST phase
+                               # 2) Didn't find SUBST and in the PST phase
+                               # In either case, return without further processing
+                               $text = $piece['text'];
+                               $found = true;
+                               $noparse = true;
+                               $noargs = true;
+                       }
+               }
+
+               # MSG, MSGNW and RAW
+               if ( !$found ) {
+                       # Check for MSGNW:
+                       $mwMsgnw =& MagicWord::get( 'msgnw' );
+                       if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
+                               $nowiki = true;
+                       } else {
+                               # Remove obsolete MSG:
+                               $mwMsg =& MagicWord::get( 'msg' );
+                               $mwMsg->matchStartAndRemove( $part1 );
+                       }
+
+                       # Check for RAW:
+                       $mwRaw =& MagicWord::get( 'raw' );
+                       if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
+                               $forceRawInterwiki = true;
+                       }
+               }
+               wfProfileOut( __METHOD__.'-modifiers' );
+
+               //save path level before recursing into functions & templates.
+               $lastPathLevel = $this->mTemplatePath;
+
+               # Parser functions
+               if ( !$found ) {
+                       wfProfileIn( __METHOD__ . '-pfunc' );
+
+                       $colonPos = strpos( $part1, ':' );
+                       if ( $colonPos !== false ) {
+                               # Case sensitive functions
+                               $function = substr( $part1, 0, $colonPos );
+                               if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
+                                       $function = $this->mFunctionSynonyms[1][$function];
+                               } else {
+                                       # Case insensitive functions
+                                       $function = strtolower( $function );
+                                       if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
+                                               $function = $this->mFunctionSynonyms[0][$function];
+                                       } else {
+                                               $function = false;
+                                       }
+                               }
+                               if ( $function ) {
+                                       $funcArgs = array_map( 'trim', $args );
+                                       $funcArgs = array_merge( array( &$this, trim( substr( $part1, $colonPos + 1 ) ) ), $funcArgs );
+                                       $result = call_user_func_array( $this->mFunctionHooks[$function], $funcArgs );
+                                       $found = true;
+
+                                       // The text is usually already parsed, doesn't need triple-brace tags expanded, etc.
+                                       //$noargs = true;
+                                       //$noparse = true;
+
+                                       if ( is_array( $result ) ) {
+                                               if ( isset( $result[0] ) ) {
+                                                       $text = $linestart . $result[0];
+                                                       unset( $result[0] );
+                                               }
+
+                                               // Extract flags into the local scope
+                                               // This allows callers to set flags such as nowiki, noparse, found, etc.
+                                               extract( $result );
+                                       } else {
+                                               $text = $linestart . $result;
+                                       }
+                               }
+                       }
+                       wfProfileOut( __METHOD__ . '-pfunc' );
+               }
+
+               # Template table test
+
+               # Did we encounter this template already? If yes, it is in the cache
+               # and we need to check for loops.
+               if ( !$found && isset( $this->mTemplates[$piece['title']] ) ) {
+                       $found = true;
+
+                       # Infinite loop test
+                       if ( isset( $this->mTemplatePath[$part1] ) ) {
+                               $noparse = true;
+                               $noargs = true;
+                               $found = true;
+                               $text = $linestart .
+                                       "[[$part1]]<!-- WARNING: template loop detected -->";
+                               wfDebug( __METHOD__.": template loop broken at '$part1'\n" );
+                       } else {
+                               # set $text to cached message.
+                               $text = $linestart . $this->mTemplates[$piece['title']];
+                               #treat title for cached page the same as others
+                               $ns = NS_TEMPLATE;
+                               $subpage = '';
+                               $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
+                               if ($subpage !== '') {
+                                 $ns = $this->mTitle->getNamespace();
+                               }
+                               $title = Title::newFromText( $part1, $ns );
+                               //used by include size checking
+                               $titleText = $title->getPrefixedText();
+                               //used by edit section links
+                               $replaceHeadings = true;
+
+                       }
+               }
+
+               # Load from database
+               if ( !$found ) {
+                       wfProfileIn( __METHOD__ . '-loadtpl' );
+                       $ns = NS_TEMPLATE;
+                       # declaring $subpage directly in the function call
+                       # does not work correctly with references and breaks
+                       # {{/subpage}}-style inclusions
+                       $subpage = '';
+                       $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
+                       if ($subpage !== '') {
+                               $ns = $this->mTitle->getNamespace();
+                       }
+                       $title = Title::newFromText( $part1, $ns );
+
+
+                       if ( !is_null( $title ) ) {
+                               $titleText = $title->getPrefixedText();
+                               # Check for language variants if the template is not found
+                               if($wgContLang->hasVariants() && $title->getArticleID() == 0){
+                                       $wgContLang->findVariantLink($part1, $title);
+                               }
+
+                               if ( !$title->isExternal() ) {
+                                       if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
+                                               $text = SpecialPage::capturePath( $title );
+                                               if ( is_string( $text ) ) {
+                                                       $found = true;
+                                                       $noparse = true;
+                                                       $noargs = true;
+                                                       $isHTML = true;
+                                                       $this->disableCache();
+                                               }
+                                       } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
+                                               $found = false; //access denied
+                                               wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
+                                       } else {
+                                               list($articleContent,$title) = $this->fetchTemplateAndtitle( $title );
+                                               if ( $articleContent !== false ) {
+                                                       $found = true;
+                                                       $text = $articleContent;
+                                                       $replaceHeadings = true;
+                                               }
+                                       }
+
+                                       # If the title is valid but undisplayable, make a link to it
+                                       if ( !$found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+                                               $text = "[[:$titleText]]";
+                                               $found = true;
+                                       }
+                               } elseif ( $title->isTrans() ) {
+                                       // Interwiki transclusion
+                                       if ( $this->ot['html'] && !$forceRawInterwiki ) {
+                                               $text = $this->interwikiTransclude( $title, 'render' );
+                                               $isHTML = true;
+                                               $noparse = true;
+                                       } else {
+                                               $text = $this->interwikiTransclude( $title, 'raw' );
+                                               $replaceHeadings = true;
+                                       }
+                                       $found = true;
+                               }
+
+                               # Template cache array insertion
+                               # Use the original $piece['title'] not the mangled $part1, so that
+                               # modifiers such as RAW: produce separate cache entries
+                               if( $found ) {
+                                       if( $isHTML ) {
+                                               // A special page; don't store it in the template cache.
+                                       } else {
+                                               $this->mTemplates[$piece['title']] = $text;
+                                       }
+                                       $text = $linestart . $text;
+                               }
+                       }
+                       wfProfileOut( __METHOD__ . '-loadtpl' );
+               }
+
+               if ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) {
+                       # Error, oversize inclusion
+                       $text = $linestart .
+                               "[[$titleText]]<!-- WARNING: template omitted, pre-expand include size too large -->";
+                       $noparse = true;
+                       $noargs = true;
+               }
+
+               # Recursive parsing, escaping and link table handling
+               # Only for HTML output
+               if ( $nowiki && $found && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+                       $text = wfEscapeWikiText( $text );
+               } elseif ( !$this->ot['msg'] && $found ) {
+                       if ( $noargs ) {
+                               $assocArgs = array();
+                       } else {
+                               # Clean up argument array
+                               $assocArgs = self::createAssocArgs($args);
+                               # Add a new element to the templace recursion path
+                               $this->mTemplatePath[$part1] = 1;
+                       }
+
+                       if ( !$noparse ) {
+                               # If there are any <onlyinclude> tags, only include them
+                               if ( in_string( '<onlyinclude>', $text ) && in_string( '</onlyinclude>', $text ) ) {
+                                       $replacer = new OnlyIncludeReplacer;
+                                       StringUtils::delimiterReplaceCallback( '<onlyinclude>', '</onlyinclude>',
+                                               array( &$replacer, 'replace' ), $text );
+                                       $text = $replacer->output;
+                               }
+                               # Remove <noinclude> sections and <includeonly> tags
+                               $text = StringUtils::delimiterReplace( '<noinclude>', '</noinclude>', '', $text );
+                               $text = strtr( $text, array( '<includeonly>' => '' , '</includeonly>' => '' ) );
+
+                               if( $this->ot['html'] || $this->ot['pre'] ) {
+                                       # Strip <nowiki>, <pre>, etc.
+                                       $text = $this->strip( $text, $this->mStripState );
+                                       if ( $this->ot['html'] ) {
+                                               $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'replaceVariables' ), $assocArgs );
+                                       } elseif ( $this->ot['pre'] && $this->mOptions->getRemoveComments() ) {
+                                               $text = Sanitizer::removeHTMLcomments( $text );
+                                       }
+                               }
+                               $text = $this->replaceVariables( $text, $assocArgs );
+
+                               # If the template begins with a table or block-level
+                               # element, it should be treated as beginning a new line.
+                               if (!$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
+                                       $text = "\n" . $text;
+                               }
+                       } elseif ( !$noargs ) {
+                               # $noparse and !$noargs
+                               # Just replace the arguments, not any double-brace items
+                               # This is used for rendered interwiki transclusion
+                               $text = $this->replaceVariables( $text, $assocArgs, true );
+                       }
+               }
+               # Prune lower levels off the recursion check path
+               $this->mTemplatePath = $lastPathLevel;
+
+               if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
+                       # Error, oversize inclusion
+                       $text = $linestart .
+                               "[[$titleText]]<!-- WARNING: template omitted, post-expand include size too large -->";
+                       $noparse = true;
+                       $noargs = true;
+               }
+
+               if ( !$found ) {
+                       wfProfileOut( $fname );
+                       return $piece['text'];
+               } else {
+                       wfProfileIn( __METHOD__ . '-placeholders' );
+                       if ( $isHTML ) {
+                               # Replace raw HTML by a placeholder
+                               # Add a blank line preceding, to prevent it from mucking up
+                               # immediately preceding headings
+                               $text = "\n\n" . $this->insertStripItem( $text, $this->mStripState );
+                       } else {
+                               # replace ==section headers==
+                               # XXX this needs to go away once we have a better parser.
+                               if ( !$this->ot['wiki'] && !$this->ot['pre'] && $replaceHeadings ) {
+                                       if( !is_null( $title ) )
+                                               $encodedname = base64_encode($title->getPrefixedDBkey());
+                                       else
+                                               $encodedname = base64_encode("");
+                                       $m = preg_split('/(^={1,6}.*?={1,6}\s*?$)/m', $text, -1,
+                                               PREG_SPLIT_DELIM_CAPTURE);
+                                       $text = '';
+                                       $nsec = $headingOffset;
+
+                                       for( $i = 0; $i < count($m); $i += 2 ) {
+                                               $text .= $m[$i];
+                                               if (!isset($m[$i + 1]) || $m[$i + 1] == "") continue;
+                                               $hl = $m[$i + 1];
+                                               if( strstr($hl, "<!--MWTEMPLATESECTION") ) {
+                                                       $text .= $hl;
+                                                       continue;
+                                               }
+                                               $m2 = array();
+                                               preg_match('/^(={1,6})(.*?)(={1,6}\s*?)$/m', $hl, $m2);
+                                               $text .= $m2[1] . $m2[2] . "<!--MWTEMPLATESECTION="
+                                                       . $encodedname . "&" . base64_encode("$nsec") . "-->" . $m2[3];
+
+                                               $nsec++;
+                                       }
+                               }
+                       }
+                       wfProfileOut( __METHOD__ . '-placeholders' );
+               }
+
+               # Prune lower levels off the recursion check path
+               $this->mTemplatePath = $lastPathLevel;
+
+               if ( !$found ) {
+                       wfProfileOut( $fname );
+                       return $piece['text'];
+               } else {
+                       wfProfileOut( $fname );
+                       return $text;
+               }
+       }
+
+       /**
+        * Fetch the unparsed text of a template and register a reference to it.
+        */
+       function fetchTemplateAndTitle( $title ) {
+               $templateCb = $this->mOptions->getTemplateCallback();
+               $stuff = call_user_func( $templateCb, $title, $this );
+               $text = $stuff['text'];
+               $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
+               if ( isset( $stuff['deps'] ) ) {
+                       foreach ( $stuff['deps'] as $dep ) {
+                               $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
+                       }
+               }
+               return array($text,$finalTitle);
+       }
+
+       function fetchTemplate( $title ) {
+               $rv = $this->fetchTemplateAndtitle($title);
+               return $rv[0];
+       }
+
+       /**
+        * Static function to get a template
+        * Can be overridden via ParserOptions::setTemplateCallback().
+        *
+        * Returns an associative array:
+        *    text          The unparsed template text
+        *    finalTitle    (Optional) The title after following redirects
+        *    deps          (Optional) An array of associative array dependencies:
+        *                       title:    The dependency title, to be registered in templatelinks
+        *                       page_id:  The page_id of the title
+        *                       rev_id:   The revision ID loaded
+        */
+       static function statelessFetchTemplate( $title, $parser=false ) {
+               $text = $skip = false;
+               $finalTitle = $title;
+               $deps = array();
+
+               // Loop to fetch the article, with up to 1 redirect
+               for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
+                       # Give extensions a chance to select the revision instead
+                       $id = false; // Assume current
+                       wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
+
+                       if( $skip ) {
+                               $text = false;
+                               $deps[] = array(
+                                       'title' => $title,
+                                       'page_id' => $title->getArticleID(),
+                                       'rev_id' => null );
+                               break;
+                       }
+                       $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
+                       $rev_id = $rev ? $rev->getId() : 0;
+
+                       $deps[] = array(
+                               'title' => $title,
+                               'page_id' => $title->getArticleID(),
+                               'rev_id' => $rev_id );
+
+                       if( $rev ) {
+                               $text = $rev->getText();
+                       } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
+                               global $wgLang;
+                               $message = $wgLang->lcfirst( $title->getText() );
+                               $text = wfMsgForContentNoTrans( $message );
+                               if( wfEmptyMsg( $message, $text ) ) {
+                                       $text = false;
+                                       break;
+                               }
+                       } else {
+                               break;
+                       }
+                       if ( $text === false ) {
+                               break;
+                       }
+                       // Redirect?
+                       $finalTitle = $title;
+                       $title = Title::newFromRedirect( $text );
+               }
+               return array(
+                       'text' => $text,
+                       'finalTitle' => $finalTitle,
+                       'deps' => $deps );
+       }
+
+       /**
+        * Transclude an interwiki link.
+        */
+       function interwikiTransclude( $title, $action ) {
+               global $wgEnableScaryTranscluding;
+
+               if (!$wgEnableScaryTranscluding)
+                       return wfMsg('scarytranscludedisabled');
+
+               $url = $title->getFullUrl( "action=$action" );
+
+               if (strlen($url) > 255)
+                       return wfMsg('scarytranscludetoolong');
+               return $this->fetchScaryTemplateMaybeFromCache($url);
+       }
+
+       function fetchScaryTemplateMaybeFromCache($url) {
+               global $wgTranscludeCacheExpiry;
+               $dbr = wfGetDB(DB_SLAVE);
+               $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
+                               array('tc_url' => $url));
+               if ($obj) {
+                       $time = $obj->tc_time;
+                       $text = $obj->tc_contents;
+                       if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
+                               return $text;
+                       }
+               }
+
+               $text = Http::get($url);
+               if (!$text)
+                       return wfMsg('scarytranscludefailed', $url);
+
+               $dbw = wfGetDB(DB_MASTER);
+               $dbw->replace('transcache', array('tc_url'), array(
+                       'tc_url' => $url,
+                       'tc_time' => time(),
+                       'tc_contents' => $text));
+               return $text;
+       }
+
+
+       /**
+        * Triple brace replacement -- used for template arguments
+        * @private
+        */
+       function argSubstitution( $matches ) {
+               $arg = trim( $matches['title'] );
+               $text = $matches['text'];
+               $inputArgs = end( $this->mArgStack );
+
+               if ( array_key_exists( $arg, $inputArgs ) ) {
+                       $text = $inputArgs[$arg];
+               } else if (($this->mOutputType == self::OT_HTML || $this->mOutputType == self::OT_PREPROCESS ) &&
+               null != $matches['parts'] && count($matches['parts']) > 0) {
+                       $text = $matches['parts'][0];
+               }
+               if ( !$this->incrementIncludeSize( 'arg', strlen( $text ) ) ) {
+                       $text = $matches['text'] .
+                               '<!-- WARNING: argument omitted, expansion size too large -->';
+               }
+
+               return $text;
+       }
+
+       /**
+        * Increment an include size counter
+        *
+        * @param string $type The type of expansion
+        * @param integer $size The size of the text
+        * @return boolean False if this inclusion would take it over the maximum, true otherwise
+        */
+       function incrementIncludeSize( $type, $size ) {
+               if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize() ) {
+                       return false;
+               } else {
+                       $this->mIncludeSizes[$type] += $size;
+                       return true;
+               }
+       }
+
+       /**
+        * Detect __NOGALLERY__ magic word and set a placeholder
+        */
+       function stripNoGallery( &$text ) {
+               # if the string __NOGALLERY__ (not case-sensitive) occurs in the HTML,
+               # do not add TOC
+               $mw = MagicWord::get( 'nogallery' );
+               $this->mOutput->mNoGallery = $mw->matchAndRemove( $text ) ;
+       }
+
+       /**
+        * Find the first __TOC__ magic word and set a <!--MWTOC-->
+        * placeholder that will then be replaced by the real TOC in
+        * ->formatHeadings, this works because at this points real
+        * comments will have already been discarded by the sanitizer.
+        *
+        * Any additional __TOC__ magic words left over will be discarded
+        * as there can only be one TOC on the page.
+        */
+       function stripToc( $text ) {
+               # if the string __NOTOC__ (not case-sensitive) occurs in the HTML,
+               # do not add TOC
+               $mw = MagicWord::get( 'notoc' );
+               if( $mw->matchAndRemove( $text ) ) {
+                       $this->mShowToc = false;
+               }
+
+               $mw = MagicWord::get( 'toc' );
+               if( $mw->match( $text ) ) {
+                       $this->mShowToc = true;
+                       $this->mForceTocPosition = true;
+
+                       // Set a placeholder. At the end we'll fill it in with the TOC.
+                       $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
+
+                       // Only keep the first one.
+                       $text = $mw->replace( '', $text );
+               }
+               return $text;
+       }
+
+       /**
+        * This function accomplishes several tasks:
+        * 1) Auto-number headings if that option is enabled
+        * 2) Add an [edit] link to sections for users who have enabled the option and can edit the page
+        * 3) Add a Table of contents on the top for users who have enabled the option
+        * 4) Auto-anchor headings
+        *
+        * It loops through all headlines, collects the necessary data, then splits up the
+        * string and re-inserts the newly formatted headlines.
+        *
+        * @param string $text
+        * @param boolean $isMain
+        * @private
+        */
+       function formatHeadings( $text, $isMain=true ) {
+               global $wgMaxTocLevel, $wgContLang;
+
+               $doNumberHeadings = $this->mOptions->getNumberHeadings();
+               if( !$this->mTitle->quickUserCan( 'edit' ) ) {
+                       $showEditLink = 0;
+               } else {
+                       $showEditLink = $this->mOptions->getEditSection();
+               }
+
+               # Inhibit editsection links if requested in the page
+               $esw =& MagicWord::get( 'noeditsection' );
+               if( $esw->matchAndRemove( $text ) ) {
+                       $showEditLink = 0;
+               }
+
+               # Get all headlines for numbering them and adding funky stuff like [edit]
+               # links - this is for later, but we need the number of headlines right now
+               $matches = array();
+               $numMatches = preg_match_all( '/<H(?P<level>[1-6])(?P<attrib>.*?'.'>)(?P<header>.*?)<\/H[1-6] *>/i', $text, $matches );
+
+               # if there are fewer than 4 headlines in the article, do not show TOC
+               # unless it's been explicitly enabled.
+               $enoughToc = $this->mShowToc &&
+                       (($numMatches >= 4) || $this->mForceTocPosition);
+
+               # Allow user to stipulate that a page should have a "new section"
+               # link added via __NEWSECTIONLINK__
+               $mw =& MagicWord::get( 'newsectionlink' );
+               if( $mw->matchAndRemove( $text ) )
+                       $this->mOutput->setNewSection( true );
+
+               # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
+               # override above conditions and always show TOC above first header
+               $mw =& MagicWord::get( 'forcetoc' );
+               if ($mw->matchAndRemove( $text ) ) {
+                       $this->mShowToc = true;
+                       $enoughToc = true;
+               }
+
+               # We need this to perform operations on the HTML
+               $sk = $this->mOptions->getSkin();
+
+               # headline counter
+               $headlineCount = 0;
+               $sectionCount = 0; # headlineCount excluding template sections
+               $numVisible = 0;
+
+               # Ugh .. the TOC should have neat indentation levels which can be
+               # passed to the skin functions. These are determined here
+               $toc = '';
+               $full = '';
+               $head = array();
+               $sublevelCount = array();
+               $levelCount = array();
+               $toclevel = 0;
+               $level = 0;
+               $prevlevel = 0;
+               $toclevel = 0;
+               $prevtoclevel = 0;
+               $tocraw = array();
+
+               foreach( $matches[3] as $headline ) {
+                       $istemplate = 0;
+                       $templatetitle = '';
+                       $templatesection = 0;
+                       $numbering = '';
+                       $mat = array();
+                       if (preg_match("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", $headline, $mat)) {
+                               $istemplate = 1;
+                               $templatetitle = base64_decode($mat[1]);
+                               $templatesection = 1 + (int)base64_decode($mat[2]);
+                               $headline = preg_replace("/<!--MWTEMPLATESECTION=([^&]+)&([^_]+)-->/", "", $headline);
+                       }
+
+                       if( $toclevel ) {
+                               $prevlevel = $level;
+                               $prevtoclevel = $toclevel;
+                       }
+                       $level = $matches[1][$headlineCount];
+
+                       if( $doNumberHeadings || $enoughToc ) {
+
+                               if ( $level > $prevlevel ) {
+                                       # Increase TOC level
+                                       $toclevel++;
+                                       $sublevelCount[$toclevel] = 0;
+                                       if( $toclevel<$wgMaxTocLevel ) {
+                                               $prevtoclevel = $toclevel;
+                                               $toc .= $sk->tocIndent();
+                                               $numVisible++;
+                                       }
+                               }
+                               elseif ( $level < $prevlevel && $toclevel > 1 ) {
+                                       # Decrease TOC level, find level to jump to
+
+                                       if ( $toclevel == 2 && $level <= $levelCount[1] ) {
+                                               # Can only go down to level 1
+                                               $toclevel = 1;
+                                       } else {
+                                               for ($i = $toclevel; $i > 0; $i--) {
+                                                       if ( $levelCount[$i] == $level ) {
+                                                               # Found last matching level
+                                                               $toclevel = $i;
+                                                               break;
+                                                       }
+                                                       elseif ( $levelCount[$i] < $level ) {
+                                                               # Found first matching level below current level
+                                                               $toclevel = $i + 1;
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                                       if( $toclevel<$wgMaxTocLevel ) {
+                                               if($prevtoclevel < $wgMaxTocLevel) {
+                                                       # Unindent only if the previous toc level was shown :p
+                                                       $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+                                               } else {
+                                                       $toc .= $sk->tocLineEnd();
+                                               }
+                                       }
+                               }
+                               else {
+                                       # No change in level, end TOC line
+                                       if( $toclevel<$wgMaxTocLevel ) {
+                                               $toc .= $sk->tocLineEnd();
+                                       }
+                               }
+
+                               $levelCount[$toclevel] = $level;
+
+                               # count number of headlines for each level
+                               @$sublevelCount[$toclevel]++;
+                               $dot = 0;
+                               for( $i = 1; $i <= $toclevel; $i++ ) {
+                                       if( !empty( $sublevelCount[$i] ) ) {
+                                               if( $dot ) {
+                                                       $numbering .= '.';
+                                               }
+                                               $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
+                                               $dot = 1;
+                                       }
+                               }
+                       }
+
+                       # The canonized header is a version of the header text safe to use for links
+                       # Avoid insertion of weird stuff like <math> by expanding the relevant sections
+                       $canonized_headline = $this->mStripState->unstripBoth( $headline );
+
+                       # Remove link placeholders by the link text.
+                       #     <!--LINK number-->
+                       # turns into
+                       #     link text with suffix
+                       $canonized_headline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
+                                                           "\$this->mLinkHolders['texts'][\$1]",
+                                                           $canonized_headline );
+                       $canonized_headline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
+                                                           "\$this->mInterwikiLinkHolders['texts'][\$1]",
+                                                           $canonized_headline );
+
+                       # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
+                       $tocline = preg_replace(
+                               array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
+                               array( '',                          '<$1>'),
+                               $canonized_headline
+                       );
+                       $tocline = trim( $tocline );
+
+                       # For the anchor, strip out HTML-y stuff period
+                       $canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline );
+                       $canonized_headline = trim( $canonized_headline );
+
+                       # Save headline for section edit hint before it's escaped
+                       $headline_hint = $canonized_headline;
+                       $canonized_headline = Sanitizer::escapeId( $canonized_headline );
+                       $refers[$headlineCount] = $canonized_headline;
+
+                       # count how many in assoc. array so we can track dupes in anchors
+                       isset( $refers[$canonized_headline] ) ? $refers[$canonized_headline]++ : $refers[$canonized_headline] = 1;
+                       $refcount[$headlineCount]=$refers[$canonized_headline];
+
+                       # Don't number the heading if it is the only one (looks silly)
+                       if( $doNumberHeadings && count( $matches[3] ) > 1) {
+                               # the two are different if the line contains a link
+                               $headline=$numbering . ' ' . $headline;
+                       }
+
+                       # Create the anchor for linking from the TOC to the section
+                       $anchor = $canonized_headline;
+                       if($refcount[$headlineCount] > 1 ) {
+                               $anchor .= '_' . $refcount[$headlineCount];
+                       }
+                       if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
+                               $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
+                               $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
+                       }
+                       # give headline the correct <h#> tag
+                       if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
+                               if( $istemplate )
+                                       $editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection);
+                               else
+                                       $editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint);
+                       } else {
+                               $editlink = '';
+                       }
+                       $head[$headlineCount] = $sk->makeHeadline( $level, $matches['attrib'][$headlineCount], $anchor, $headline, $editlink );
+
+                       $headlineCount++;
+                       if( !$istemplate )
+                               $sectionCount++;
+               }
+
+               $this->mOutput->setSections( $tocraw );
+
+               # Never ever show TOC if no headers
+               if( $numVisible < 1 ) {
+                       $enoughToc = false;
+               }
+
+               if( $enoughToc ) {
+                       if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
+                               $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
+                       }
+                       $toc = $sk->tocList( $toc );
+               }
+
+               # split up and insert constructed headlines
+
+               $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
+               $i = 0;
+
+               foreach( $blocks as $block ) {
+                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
+                               # This is the [edit] link that appears for the top block of text when
+                               # section editing is enabled
+
+                               # Disabled because it broke block formatting
+                               # For example, a bullet point in the top line
+                               # $full .= $sk->editSectionLink(0);
+                       }
+                       $full .= $block;
+                       if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
+                               # Top anchor now in skin
+                               $full = $full.$toc;
+                       }
+
+                       if( !empty( $head[$i] ) ) {
+                               $full .= $head[$i];
+                       }
+                       $i++;
+               }
+               if( $this->mForceTocPosition ) {
+                       return str_replace( '<!--MWTOC-->', $toc, $full );
+               } else {
+                       return $full;
+               }
+       }
+
+       /**
+        * Transform wiki markup when saving a page by doing \r\n -> \n
+        * conversion, substitting signatures, {{subst:}} templates, etc.
+        *
+        * @param string $text the text to transform
+        * @param Title &$title the Title object for the current article
+        * @param User &$user the User object describing the current user
+        * @param ParserOptions $options parsing options
+        * @param bool $clearState whether to clear the parser state first
+        * @return string the altered wiki markup
+        * @public
+        */
+       function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
+               $this->mOptions = $options;
+               $this->mTitle =& $title;
+               $this->setOutputType( self::OT_WIKI );
+
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+
+               $stripState = new StripState;
+               $pairs = array(
+                       "\r\n" => "\n",
+               );
+               $text = str_replace( array_keys( $pairs ), array_values( $pairs ), $text );
+               $text = $this->strip( $text, $stripState, true, array( 'gallery' ) );
+               $text = $this->pstPass2( $text, $stripState, $user );
+               $text = $stripState->unstripBoth( $text );
+               return $text;
+       }
+
+       /**
+        * Pre-save transform helper function
+        * @private
+        */
+       function pstPass2( $text, &$stripState, $user ) {
+               global $wgContLang, $wgLocaltimezone;
+
+               /* Note: This is the timestamp saved as hardcoded wikitext to
+                * the database, we use $wgContLang here in order to give
+                * everyone the same signature and use the default one rather
+                * than the one selected in each user's preferences.
+                */
+               if ( isset( $wgLocaltimezone ) ) {
+                       $oldtz = getenv( 'TZ' );
+                       putenv( 'TZ='.$wgLocaltimezone );
+               }
+               $d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) .
+                 ' (' . date( 'T' ) . ')';
+               if ( isset( $wgLocaltimezone ) ) {
+                       putenv( 'TZ='.$oldtz );
+               }
+
+               # Variable replacement
+               # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+               $text = $this->replaceVariables( $text );
+
+               # Strip out <nowiki> etc. added via replaceVariables
+               $text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
+
+               # Signatures
+               $sigText = $this->getUserSig( $user );
+               $text = strtr( $text, array(
+                       '~~~~~' => $d,
+                       '~~~~' => "$sigText $d",
+                       '~~~' => $sigText
+               ) );
+
+               # Context links: [[|name]] and [[name (context)|]]
+               #
+               global $wgLegalTitleChars;
+               $tc = "[$wgLegalTitleChars]";
+               $nc = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
+
+               $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\))\\|]]/";            # [[ns:page (context)|]]
+               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( \\($tc+\\)|)(, $tc+|)\\|]]/";  # [[ns:page (context), context|]]
+               $p2 = "/\[\[\\|($tc+)]]/";                                      # [[|page]]
+
+               # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
+               $text = preg_replace( $p1, '[[\\1\\2\\3|\\2]]', $text );
+               $text = preg_replace( $p3, '[[\\1\\2\\3\\4|\\2]]', $text );
+
+               $t = $this->mTitle->getText();
+               $m = array();
+               if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
+                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
+               } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
+                       $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
+               } else {
+                       # if there's no context, don't bother duplicating the title
+                       $text = preg_replace( $p2, '[[\\1]]', $text );
+               }
+
+               # Trim trailing whitespace
+               $text = rtrim( $text );
+
+               return $text;
+       }
+
+       /**
+        * Fetch the user's signature text, if any, and normalize to
+        * validated, ready-to-insert wikitext.
+        *
+        * @param User $user
+        * @return string
+        * @private
+        */
+       function getUserSig( &$user ) {
+               global $wgMaxSigChars;
+
+               $username = $user->getName();
+               $nickname = $user->getOption( 'nickname' );
+               $nickname = $nickname === '' ? $username : $nickname;
+
+               if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
+                       $nickname = $username;
+                       wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
+               } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
+                       # Sig. might contain markup; validate this
+                       if( $this->validateSig( $nickname ) !== false ) {
+                               # Validated; clean up (if needed) and return it
+                               return $this->cleanSig( $nickname, true );
+                       } else {
+                               # Failed to validate; fall back to the default
+                               $nickname = $username;
+                               wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+                       }
+               }
+
+               // Make sure nickname doesnt get a sig in a sig
+               $nickname = $this->cleanSigInSig( $nickname );
+
+               # If we're still here, make it a link to the user page
+               $userText = wfEscapeWikiText( $username );
+               $nickText = wfEscapeWikiText( $nickname );
+               if ( $user->isAnon() )  {
+                       return wfMsgExt( 'signature-anon', array( 'content', 'parsemag' ), $userText, $nickText );
+               } else {
+                       return wfMsgExt( 'signature', array( 'content', 'parsemag' ), $userText, $nickText );
+               }
+       }
+
+       /**
+        * Check that the user's signature contains no bad XML
+        *
+        * @param string $text
+        * @return mixed An expanded string, or false if invalid.
+        */
+       function validateSig( $text ) {
+               return( wfIsWellFormedXmlFragment( $text ) ? $text : false );
+       }
+
+       /**
+        * Clean up signature text
+        *
+        * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
+        * 2) Substitute all transclusions
+        *
+        * @param string $text
+        * @param $parsing Whether we're cleaning (preferences save) or parsing
+        * @return string Signature text
+        */
+       function cleanSig( $text, $parsing = false ) {
+               global $wgTitle;
+               $this->startExternalParse( $this->mTitle, new ParserOptions(), $parsing ? self::OT_WIKI : self::OT_MSG );
+
+               $substWord = MagicWord::get( 'subst' );
+               $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
+               $substText = '{{' . $substWord->getSynonym( 0 );
+
+               $text = preg_replace( $substRegex, $substText, $text );
+               $text = $this->cleanSigInSig( $text );
+               $text = $this->replaceVariables( $text );
+
+               $this->clearState();
+               return $text;
+       }
+
+       /**
+        * Strip ~~~, ~~~~ and ~~~~~ out of signatures
+        * @param string $text
+        * @return string Signature text with /~{3,5}/ removed
+        */
+       function cleanSigInSig( $text ) {
+               $text = preg_replace( '/~{3,5}/', '', $text );
+               return $text;
+       }
+
+       /**
+        * Set up some variables which are usually set up in parse()
+        * so that an external function can call some class members with confidence
+        * @public
+        */
+       function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
+               $this->mTitle =& $title;
+               $this->mOptions = $options;
+               $this->setOutputType( $outputType );
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+       }
+
+       /**
+        * Transform a MediaWiki message by replacing magic variables.
+        *
+        * @param string $text the text to transform
+        * @param ParserOptions $options  options
+        * @return string the text with variables substituted
+        * @public
+        */
+       function transformMsg( $text, $options ) {
+               global $wgTitle;
+               static $executing = false;
+
+               $fname = "Parser::transformMsg";
+
+               # Guard against infinite recursion
+               if ( $executing ) {
+                       return $text;
+               }
+               $executing = true;
+
+               wfProfileIn($fname);
+
+               if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) {
+                       $this->mTitle = $wgTitle;
+               } else {
+                       $this->mTitle = Title::newFromText('msg');
+               }
+               $this->mOptions = $options;
+               $this->setOutputType( self::OT_MSG );
+               $this->clearState();
+               $text = $this->replaceVariables( $text );
+
+               $executing = false;
+               wfProfileOut($fname);
+               return $text;
+       }
+
+       /**
+        * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
+        * The callback should have the following form:
+        *    function myParserHook( $text, $params, &$parser ) { ... }
+        *
+        * Transform and return $text. Use $parser for any required context, e.g. use
+        * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
+        *
+        * @public
+        *
+        * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
+        * @param mixed $callback The callback function (and object) to use for the tag
+        *
+        * @return The old value of the mTagHooks array associated with the hook
+        */
+       function setHook( $tag, $callback ) {
+               $tag = strtolower( $tag );
+               $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
+               $this->mTagHooks[$tag] = $callback;
+
+               return $oldVal;
+       }
+
+       function setTransparentTagHook( $tag, $callback ) {
+               $tag = strtolower( $tag );
+               $oldVal = isset( $this->mTransparentTagHooks[$tag] ) ? $this->mTransparentTagHooks[$tag] : null;
+               $this->mTransparentTagHooks[$tag] = $callback;
+
+               return $oldVal;
+       }
+
+       /**
+        * Create a function, e.g. {{sum:1|2|3}}
+        * The callback function should have the form:
+        *    function myParserFunction( &$parser, $arg1, $arg2, $arg3 ) { ... }
+        *
+        * The callback may either return the text result of the function, or an array with the text
+        * in element 0, and a number of flags in the other elements. The names of the flags are
+        * specified in the keys. Valid flags are:
+        *   found                     The text returned is valid, stop processing the template. This
+        *                             is on by default.
+        *   nowiki                    Wiki markup in the return value should be escaped
+        *   noparse                   Unsafe HTML tags should not be stripped, etc.
+        *   noargs                    Don't replace triple-brace arguments in the return value
+        *   isHTML                    The returned text is HTML, armour it against wikitext transformation
+        *
+        * @public
+        *
+        * @param string $id The magic word ID
+        * @param mixed $callback The callback function (and object) to use
+        * @param integer $flags a combination of the following flags:
+        *                SFH_NO_HASH No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
+        *
+        * @return The old callback function for this name, if any
+        */
+       function setFunctionHook( $id, $callback, $flags = 0 ) {
+               $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id] : null;
+               $this->mFunctionHooks[$id] = $callback;
+
+               # Add to function cache
+               $mw = MagicWord::get( $id );
+               if( !$mw )
+                       throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
+
+               $synonyms = $mw->getSynonyms();
+               $sensitive = intval( $mw->isCaseSensitive() );
+
+               foreach ( $synonyms as $syn ) {
+                       # Case
+                       if ( !$sensitive ) {
+                               $syn = strtolower( $syn );
+                       }
+                       # Add leading hash
+                       if ( !( $flags & SFH_NO_HASH ) ) {
+                               $syn = '#' . $syn;
+                       }
+                       # Remove trailing colon
+                       if ( substr( $syn, -1, 1 ) == ':' ) {
+                               $syn = substr( $syn, 0, -1 );
+                       }
+                       $this->mFunctionSynonyms[$sensitive][$syn] = $id;
+               }
+               return $oldVal;
+       }
+
+       /**
+        * Get all registered function hook identifiers
+        *
+        * @return array
+        */
+       function getFunctionHooks() {
+               return array_keys( $this->mFunctionHooks );
+       }
+
+       /**
+        * Replace <!--LINK--> link placeholders with actual links, in the buffer
+        * Placeholders created in Skin::makeLinkObj()
+        * Returns an array of links found, indexed by PDBK:
+        *  0 - broken
+        *  1 - normal link
+        *  2 - stub
+        * $options is a bit field, RLH_FOR_UPDATE to select for update
+        */
+       function replaceLinkHolders( &$text, $options = 0 ) {
+               global $wgUser;
+               global $wgContLang;
+
+               $fname = 'Parser::replaceLinkHolders';
+               wfProfileIn( $fname );
+
+               $pdbks = array();
+               $colours = array();
+               $sk = $this->mOptions->getSkin();
+               $linkCache = LinkCache::singleton();
+
+               if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
+                       wfProfileIn( $fname.'-check' );
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $page = $dbr->tableName( 'page' );
+                       $threshold = $wgUser->getOption('stubthreshold');
+
+                       # Sort by namespace
+                       asort( $this->mLinkHolders['namespaces'] );
+
+                       # Generate query
+                       $query = false;
+                       $current = null;
+                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+                               # Make title object
+                               $title = $this->mLinkHolders['titles'][$key];
+
+                               # Skip invalid entries.
+                               # Result will be ugly, but prevents crash.
+                               if ( is_null( $title ) ) {
+                                       continue;
+                               }
+                               $pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
+
+                               # Check if it's a static known link, e.g. interwiki
+                               if ( $title->isAlwaysKnown() ) {
+                                       $colours[$pdbk] = 1;
+                               } elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
+                                       $colours[$pdbk] = 1;
+                                       $this->mOutput->addLink( $title, $id );
+                               } elseif ( $linkCache->isBadLink( $pdbk ) ) {
+                                       $colours[$pdbk] = 0;
+                               } elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
+                                       $colours[$pdbk] = 0;
+                               } else {
+                                       # Not in the link cache, add it to the query
+                                       if ( !isset( $current ) ) {
+                                               $current = $ns;
+                                               $query =  "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect";
+                                               $query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
+                                       } elseif ( $current != $ns ) {
+                                               $current = $ns;
+                                               $query .= ")) OR (page_namespace=$ns AND page_title IN(";
+                                       } else {
+                                               $query .= ', ';
+                                       }
+
+                                       $query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
+                               }
+                       }
+                       if ( $query ) {
+                               $query .= '))';
+                               if ( $options & RLH_FOR_UPDATE ) {
+                                       $query .= ' FOR UPDATE';
+                               }
+
+                               $res = $dbr->query( $query, $fname );
+
+                               # Fetch data and form into an associative array
+                               # non-existent = broken
+                               # 1 = known
+                               # 2 = stub
+                               while ( $s = $dbr->fetchObject($res) ) {
+                                       $title = Title::makeTitle( $s->page_namespace, $s->page_title );
+                                       $pdbk = $title->getPrefixedDBkey();
+                                       $linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
+                                       $this->mOutput->addLink( $title, $s->page_id );
+
+                                       $colours[$pdbk] = ( $threshold == 0 || (
+                                                               $s->page_len >= $threshold || # always true if $threshold <= 0
+                                                               $s->page_is_redirect ||
+                                                               !MWNamespace::isContent( $s->page_namespace ) )
+                                                           ? 1 : 2 );
+                               }
+                       }
+                       wfProfileOut( $fname.'-check' );
+
+                       # Do a second query for different language variants of links and categories
+                       if( $wgContLang->hasVariants() ) {
+                               $linkBatch = new LinkBatch();
+                               $variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
+                               $categoryMap = array(); // maps $category_variant => $category (dbkeys)
+                               $varCategories = array(); // category replacements oldDBkey => newDBkey
+
+                               $categories = $this->mOutput->getCategoryLinks();
+
+                               // Add variants of links to link batch
+                               foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+                                       $title = $this->mLinkHolders['titles'][$key];
+                                       if ( is_null( $title ) )
+                                               continue;
+
+                                       $pdbk = $title->getPrefixedDBkey();
+                                       $titleText = $title->getText();
+
+                                       // generate all variants of the link title text
+                                       $allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
+
+                                       // if link was not found (in first query), add all variants to query
+                                       if ( !isset($colours[$pdbk]) ){
+                                               foreach($allTextVariants as $textVariant){
+                                                       if($textVariant != $titleText){
+                                                               $variantTitle = Title::makeTitle( $ns, $textVariant );
+                                                               if(is_null($variantTitle)) continue;
+                                                               $linkBatch->addObj( $variantTitle );
+                                                               $variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               // process categories, check if a category exists in some variant
+                               foreach( $categories as $category ){
+                                       $variants = $wgContLang->convertLinkToAllVariants($category);
+                                       foreach($variants as $variant){
+                                               if($variant != $category){
+                                                       $variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
+                                                       if(is_null($variantTitle)) continue;
+                                                       $linkBatch->addObj( $variantTitle );
+                                                       $categoryMap[$variant] = $category;
+                                               }
+                                       }
+                               }
+
+
+                               if ( !$linkBatch->isEmpty() ){
+                                       // construct query
+                                       $titleClause = $linkBatch->constructSet('page', $dbr);
+
+                                       $variantQuery =  "SELECT page_id, page_namespace, page_title, page_len, page_is_redirect";
+
+                                       $variantQuery .= " FROM $page WHERE $titleClause";
+                                       if ( $options & RLH_FOR_UPDATE ) {
+                                               $variantQuery .= ' FOR UPDATE';
+                                       }
+
+                                       $varRes = $dbr->query( $variantQuery, $fname );
+
+                                       // for each found variants, figure out link holders and replace
+                                       while ( $s = $dbr->fetchObject($varRes) ) {
+
+                                               $variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
+                                               $varPdbk = $variantTitle->getPrefixedDBkey();
+                                               $vardbk = $variantTitle->getDBkey();
+
+                                               $holderKeys = array();
+                                               if(isset($variantMap[$varPdbk])){
+                                                       $holderKeys = $variantMap[$varPdbk];
+                                                       $linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
+                                                       $this->mOutput->addLink( $variantTitle, $s->page_id );
+                                               }
+
+                                               // loop over link holders
+                                               foreach($holderKeys as $key){
+                                                       $title = $this->mLinkHolders['titles'][$key];
+                                                       if ( is_null( $title ) ) continue;
+
+                                                       $pdbk = $title->getPrefixedDBkey();
+
+                                                       if(!isset($colours[$pdbk])){
+                                                               // found link in some of the variants, replace the link holder data
+                                                               $this->mLinkHolders['titles'][$key] = $variantTitle;
+                                                               $this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
+
+                                                               // set pdbk and colour
+                                                               $pdbks[$key] = $varPdbk;
+                                                               if ( $threshold >  0 ) {
+                                                                       $size = $s->page_len;
+                                                                       if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) {
+                                                                               $colours[$varPdbk] = 1;
+                                                                       } else {
+                                                                               $colours[$varPdbk] = 2;
+                                                                       }
+                                                               }
+                                                               else {
+                                                                       $colours[$varPdbk] = 1;
+                                                               }
+                                                       }
+                                               }
+
+                                               // check if the object is a variant of a category
+                                               if(isset($categoryMap[$vardbk])){
+                                                       $oldkey = $categoryMap[$vardbk];
+                                                       if($oldkey != $vardbk)
+                                                               $varCategories[$oldkey]=$vardbk;
+                                               }
+                                       }
+
+                                       // rebuild the categories in original order (if there are replacements)
+                                       if(count($varCategories)>0){
+                                               $newCats = array();
+                                               $originalCats = $this->mOutput->getCategories();
+                                               foreach($originalCats as $cat => $sortkey){
+                                                       // make the replacement
+                                                       if( array_key_exists($cat,$varCategories) )
+                                                               $newCats[$varCategories[$cat]] = $sortkey;
+                                                       else $newCats[$cat] = $sortkey;
+                                               }
+                                               $this->mOutput->setCategoryLinks($newCats);
+                                       }
+                               }
+                       }
+
+                       # Construct search and replace arrays
+                       wfProfileIn( $fname.'-construct' );
+                       $replacePairs = array();
+                       foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
+                               $pdbk = $pdbks[$key];
+                               $searchkey = "<!--LINK $key-->";
+                               $title = $this->mLinkHolders['titles'][$key];
+                               if ( empty( $colours[$pdbk] ) ) {
+                                       $linkCache->addBadLinkObj( $title );
+                                       $colours[$pdbk] = 0;
+                                       $this->mOutput->addLink( $title, 0 );
+                                       $replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
+                                                                       $this->mLinkHolders['texts'][$key],
+                                                                       $this->mLinkHolders['queries'][$key] );
+                               } elseif ( $colours[$pdbk] == 1 ) {
+                                       $replacePairs[$searchkey] = $sk->makeKnownLinkObj( $title,
+                                                                       $this->mLinkHolders['texts'][$key],
+                                                                       $this->mLinkHolders['queries'][$key] );
+                               } elseif ( $colours[$pdbk] == 2 ) {
+                                       $replacePairs[$searchkey] = $sk->makeStubLinkObj( $title,
+                                                                       $this->mLinkHolders['texts'][$key],
+                                                                       $this->mLinkHolders['queries'][$key] );
+                               }
+                       }
+                       $replacer = new HashtableReplacer( $replacePairs, 1 );
+                       wfProfileOut( $fname.'-construct' );
+
+                       # Do the thing
+                       wfProfileIn( $fname.'-replace' );
+                       $text = preg_replace_callback(
+                               '/(<!--LINK .*?-->)/',
+                               $replacer->cb(),
+                               $text);
+
+                       wfProfileOut( $fname.'-replace' );
+               }
+
+               # Now process interwiki link holders
+               # This is quite a bit simpler than internal links
+               if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
+                       wfProfileIn( $fname.'-interwiki' );
+                       # Make interwiki link HTML
+                       $replacePairs = array();
+                       foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
+                               $title = $this->mInterwikiLinkHolders['titles'][$key];
+                               $replacePairs[$key] = $sk->makeLinkObj( $title, $link );
+                       }
+                       $replacer = new HashtableReplacer( $replacePairs, 1 );
+
+                       $text = preg_replace_callback(
+                               '/<!--IWLINK (.*?)-->/',
+                               $replacer->cb(),
+                               $text );
+                       wfProfileOut( $fname.'-interwiki' );
+               }
+
+               wfProfileOut( $fname );
+               return $colours;
+       }
+
+       /**
+        * Replace <!--LINK--> link placeholders with plain text of links
+        * (not HTML-formatted).
+        * @param string $text
+        * @return string
+        */
+       function replaceLinkHoldersText( $text ) {
+               $fname = 'Parser::replaceLinkHoldersText';
+               wfProfileIn( $fname );
+
+               $text = preg_replace_callback(
+                       '/<!--(LINK|IWLINK) (.*?)-->/',
+                       array( &$this, 'replaceLinkHoldersTextCallback' ),
+                       $text );
+
+               wfProfileOut( $fname );
+               return $text;
+       }
+
+       /**
+        * @param array $matches
+        * @return string
+        * @private
+        */
+       function replaceLinkHoldersTextCallback( $matches ) {
+               $type = $matches[1];
+               $key  = $matches[2];
+               if( $type == 'LINK' ) {
+                       if( isset( $this->mLinkHolders['texts'][$key] ) ) {
+                               return $this->mLinkHolders['texts'][$key];
+                       }
+               } elseif( $type == 'IWLINK' ) {
+                       if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
+                               return $this->mInterwikiLinkHolders['texts'][$key];
+                       }
+               }
+               return $matches[0];
+       }
+
+       /**
+        * Tag hook handler for 'pre'.
+        */
+       function renderPreTag( $text, $attribs ) {
+               // Backwards-compatibility hack
+               $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
+
+               $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
+               return wfOpenElement( 'pre', $attribs ) .
+                       Xml::escapeTagsOnly( $content ) .
+                       '</pre>';
+       }
+
+       /**
+        * Renders an image gallery from a text with one line per image.
+        * text labels may be given by using |-style alternative text. E.g.
+        *   Image:one.jpg|The number "1"
+        *   Image:tree.jpg|A tree
+        * given as text will return the HTML of a gallery with two images,
+        * labeled 'The number "1"' and
+        * 'A tree'.
+        */
+       function renderImageGallery( $text, $params ) {
+               $ig = new ImageGallery();
+               $ig->setContextTitle( $this->mTitle );
+               $ig->setShowBytes( false );
+               $ig->setShowFilename( false );
+               $ig->setParser( $this );
+               $ig->setHideBadImages();
+               $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
+               $ig->useSkin( $this->mOptions->getSkin() );
+               $ig->mRevisionId = $this->mRevisionId;
+
+               if( isset( $params['caption'] ) ) {
+                       $caption = $params['caption'];
+                       $caption = htmlspecialchars( $caption );
+                       $caption = $this->replaceInternalLinks( $caption );
+                       $ig->setCaptionHtml( $caption );
+               }
+               if( isset( $params['perrow'] ) ) {
+                       $ig->setPerRow( $params['perrow'] );
+               }
+               if( isset( $params['widths'] ) ) {
+                       $ig->setWidths( $params['widths'] );
+               }
+               if( isset( $params['heights'] ) ) {
+                       $ig->setHeights( $params['heights'] );
+               }
+
+               wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
+
+               $lines = explode( "\n", $text );
+               foreach ( $lines as $line ) {
+                       # match lines like these:
+                       # Image:someimage.jpg|This is some image
+                       $matches = array();
+                       preg_match( "/^([^|]+)(\\|(.*))?$/", $line, $matches );
+                       # Skip empty lines
+                       if ( count( $matches ) == 0 ) {
+                               continue;
+                       }
+                       $tp = Title::newFromText( $matches[1] );
+                       $nt =& $tp;
+                       if( is_null( $nt ) ) {
+                               # Bogus title. Ignore these so we don't bomb out later.
+                               continue;
+                       }
+                       if ( isset( $matches[3] ) ) {
+                               $label = $matches[3];
+                       } else {
+                               $label = '';
+                       }
+
+                       $pout = $this->parse( $label,
+                               $this->mTitle,
+                               $this->mOptions,
+                               false, // Strip whitespace...?
+                               false  // Don't clear state!
+                       );
+                       $html = $pout->getText();
+
+                       $ig->add( $nt, $html );
+
+                       # Only add real images (bug #5586)
+                       if ( $nt->getNamespace() == NS_IMAGE ) {
+                               $this->mOutput->addImage( $nt->getDBkey() );
+                       }
+               }
+               return $ig->toHTML();
+       }
+
+       function getImageParams( $handler ) {
+               if ( $handler ) {
+                       $handlerClass = get_class( $handler );
+               } else {
+                       $handlerClass = '';
+               }
+               if ( !isset( $this->mImageParams[$handlerClass]  ) ) {
+                       // Initialise static lists
+                       static $internalParamNames = array(
+                               'horizAlign' => array( 'left', 'right', 'center', 'none' ),
+                               'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
+                                       'bottom', 'text-bottom' ),
+                               'frame' => array( 'thumbnail', 'manualthumb', 'framed', 'frameless',
+                                       'upright', 'border' ),
+                       );
+                       static $internalParamMap;
+                       if ( !$internalParamMap ) {
+                               $internalParamMap = array();
+                               foreach ( $internalParamNames as $type => $names ) {
+                                       foreach ( $names as $name ) {
+                                               $magicName = str_replace( '-', '_', "img_$name" );
+                                               $internalParamMap[$magicName] = array( $type, $name );
+                                       }
+                               }
+                       }
+
+                       // Add handler params
+                       $paramMap = $internalParamMap;
+                       if ( $handler ) {
+                               $handlerParamMap = $handler->getParamMap();
+                               foreach ( $handlerParamMap as $magic => $paramName ) {
+                                       $paramMap[$magic] = array( 'handler', $paramName );
+                               }
+                       }
+                       $this->mImageParams[$handlerClass] = $paramMap;
+                       $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
+               }
+               return array( $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] );
+       }
+
+       /**
+        * Parse image options text and use it to make an image
+        */
+       function makeImage( $title, $options ) {
+               # @TODO: let the MediaHandler specify its transform parameters
+               #
+               # Check if the options text is of the form "options|alt text"
+               # Options are:
+               #  * thumbnail          make a thumbnail with enlarge-icon and caption, alignment depends on lang
+               #  * left               no resizing, just left align. label is used for alt= only
+               #  * right              same, but right aligned
+               #  * none               same, but not aligned
+               #  * ___px              scale to ___ pixels width, no aligning. e.g. use in taxobox
+               #  * center             center the image
+               #  * framed             Keep original image size, no magnify-button.
+               #  * frameless          like 'thumb' but without a frame. Keeps user preferences for width
+               #  * upright            reduce width for upright images, rounded to full __0 px
+               #  * border             draw a 1px border around the image
+               # vertical-align values (no % or length right now):
+               #  * baseline
+               #  * sub
+               #  * super
+               #  * top
+               #  * text-top
+               #  * middle
+               #  * bottom
+               #  * text-bottom
+
+               $parts = array_map( 'trim', explode( '|', $options) );
+               $sk = $this->mOptions->getSkin();
+
+               # Give extensions a chance to select the file revision for us
+               $skip = $time = false;
+               wfRunHooks( 'BeforeParserMakeImageLinkObj', array( &$this, &$title, &$skip, &$time ) );
+
+               if ( $skip ) {
+                       return $sk->makeLinkObj( $title );
+               }
+
+               # Get parameter map
+               $file = wfFindFile( $title, $time );
+               $handler = $file ? $file->getHandler() : false;
+
+               list( $paramMap, $mwArray ) = $this->getImageParams( $handler );
+
+               # Process the input parameters
+               $caption = '';
+               $params = array( 'frame' => array(), 'handler' => array(),
+                       'horizAlign' => array(), 'vertAlign' => array() );
+               foreach( $parts as $part ) {
+                       list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
+                       if ( isset( $paramMap[$magicName] ) ) {
+                               list( $type, $paramName ) = $paramMap[$magicName];
+                               $params[$type][$paramName] = $value;
+
+                               // Special case; width and height come in one variable together
+                               if( $type == 'handler' && $paramName == 'width' ) {
+                                       $m = array();
+                                       if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) {
+                                               $params[$type]['width'] = intval( $m[1] );
+                                               $params[$type]['height'] = intval( $m[2] );
+                                       } else {
+                                               $params[$type]['width'] = intval( $value );
+                                       }
+                               }
+                       } else {
+                               $caption = $part;
+                       }
+               }
+
+               # Process alignment parameters
+               if ( $params['horizAlign'] ) {
+                       $params['frame']['align'] = key( $params['horizAlign'] );
+               }
+               if ( $params['vertAlign'] ) {
+                       $params['frame']['valign'] = key( $params['vertAlign'] );
+               }
+
+               # Validate the handler parameters
+               if ( $handler ) {
+                       foreach ( $params['handler'] as $name => $value ) {
+                               if ( !$handler->validateParam( $name, $value ) ) {
+                                       unset( $params['handler'][$name] );
+                               }
+                       }
+               }
+
+               # Strip bad stuff out of the alt text
+               $alt = $this->replaceLinkHoldersText( $caption );
+
+               # make sure there are no placeholders in thumbnail attributes
+               # that are later expanded to html- so expand them now and
+               # remove the tags
+               $alt = $this->mStripState->unstripBoth( $alt );
+               $alt = Sanitizer::stripAllTags( $alt );
+
+               $params['frame']['alt'] = $alt;
+               $params['frame']['caption'] = $caption;
+
+               # Linker does the rest
+               $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'] );
+
+               # Give the handler a chance to modify the parser object
+               if ( $handler ) {
+                       $handler->parserTransformHook( $this, $file );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Set a flag in the output object indicating that the content is dynamic and
+        * shouldn't be cached.
+        */
+       function disableCache() {
+               wfDebug( "Parser output marked as uncacheable.\n" );
+               $this->mOutput->mCacheTime = -1;
+       }
+
+       /**#@+
+        * Callback from the Sanitizer for expanding items found in HTML attribute
+        * values, so they can be safely tested and escaped.
+        * @param string $text
+        * @param array $args
+        * @return string
+        * @private
+        */
+       function attributeStripCallback( &$text, $args ) {
+               $text = $this->replaceVariables( $text, $args );
+               $text = $this->mStripState->unstripBoth( $text );
+               return $text;
+       }
+
+       /**#@-*/
+
+       /**#@+
+        * Accessor/mutator
+        */
+       function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
+       function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
+       function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
+       /**#@-*/
+
+       /**#@+
+        * Accessor
+        */
+       function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
+       /**#@-*/
+
+
+       /**
+        * Break wikitext input into sections, and either pull or replace
+        * some particular section's text.
+        *
+        * External callers should use the getSection and replaceSection methods.
+        *
+        * @param $text Page wikitext
+        * @param $section Numbered section. 0 pulls the text before the first
+        *                 heading; other numbers will pull the given section
+        *                 along with its lower-level subsections.
+        * @param $mode One of "get" or "replace"
+        * @param $newtext Replacement text for section data.
+        * @return string for "get", the extracted section text.
+        *                for "replace", the whole page with the section replaced.
+        */
+       private function extractSections( $text, $section, $mode, $newtext='' ) {
+               # I.... _hope_ this is right.
+               # Otherwise, sometimes we don't have things initialized properly.
+               $this->clearState();
+
+               # strip NOWIKI etc. to avoid confusion (true-parameter causes HTML
+               # comments to be stripped as well)
+               $stripState = new StripState;
+
+               $oldOutputType = $this->mOutputType;
+               $oldOptions = $this->mOptions;
+               $this->mOptions = new ParserOptions();
+               $this->setOutputType( self::OT_WIKI );
+
+               $striptext = $this->strip( $text, $stripState, true );
+
+               $this->setOutputType( $oldOutputType );
+               $this->mOptions = $oldOptions;
+
+               # now that we can be sure that no pseudo-sections are in the source,
+               # split it up by section
+               $uniq = preg_quote( $this->uniqPrefix(), '/' );
+               $comment = "(?:$uniq-!--.*?QINU\x07)";
+               $secs = preg_split(
+                       "/
+                       (
+                               ^
+                               (?:$comment|<\/?noinclude>)* # Initial comments will be stripped
+                               (=+) # Should this be limited to 6?
+                               .+?  # Section title...
+                               \\2  # Ending = count must match start
+                               (?:$comment|<\/?noinclude>|[ \\t]+)* # Trailing whitespace ok
+                               $
+                       |
+                               <h([1-6])\b.*?>
+                               .*?
+                               <\/h\\3\s*>
+                       )
+                       /mix",
+                       $striptext, -1,
+                       PREG_SPLIT_DELIM_CAPTURE);
+
+               if( $mode == "get" ) {
+                       if( $section == 0 ) {
+                               // "Section 0" returns the content before any other section.
+                               $rv = $secs[0];
+                       } else {
+                               //track missing section, will replace if found.
+                               $rv = $newtext;
+                       }
+               } elseif( $mode == "replace" ) {
+                       if( $section == 0 ) {
+                               $rv = $newtext . "\n\n";
+                               $remainder = true;
+                       } else {
+                               $rv = $secs[0];
+                               $remainder = false;
+                       }
+               }
+               $count = 0;
+               $sectionLevel = 0;
+               for( $index = 1; $index < count( $secs ); ) {
+                       $headerLine = $secs[$index++];
+                       if( $secs[$index] ) {
+                               // A wiki header
+                               $headerLevel = strlen( $secs[$index++] );
+                       } else {
+                               // An HTML header
+                               $index++;
+                               $headerLevel = intval( $secs[$index++] );
+                       }
+                       $content = $secs[$index++];
+
+                       $count++;
+                       if( $mode == "get" ) {
+                               if( $count == $section ) {
+                                       $rv = $headerLine . $content;
+                                       $sectionLevel = $headerLevel;
+                               } elseif( $count > $section ) {
+                                       if( $sectionLevel && $headerLevel > $sectionLevel ) {
+                                               $rv .= $headerLine . $content;
+                                       } else {
+                                               // Broke out to a higher-level section
+                                               break;
+                                       }
+                               }
+                       } elseif( $mode == "replace" ) {
+                               if( $count < $section ) {
+                                       $rv .= $headerLine . $content;
+                               } elseif( $count == $section ) {
+                                       $rv .= $newtext . "\n\n";
+                                       $sectionLevel = $headerLevel;
+                               } elseif( $count > $section ) {
+                                       if( $headerLevel <= $sectionLevel ) {
+                                               // Passed the section's sub-parts.
+                                               $remainder = true;
+                                       }
+                                       if( $remainder ) {
+                                               $rv .= $headerLine . $content;
+                                       }
+                               }
+                       }
+               }
+               if (is_string($rv))
+                       # reinsert stripped tags
+                       $rv = trim( $stripState->unstripBoth( $rv ) );
+
+               return $rv;
+       }
+
+       /**
+        * This function returns the text of a section, specified by a number ($section).
+        * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
+        * the first section before any such heading (section 0).
+        *
+        * If a section contains subsections, these are also returned.
+        *
+        * @param $text String: text to look in
+        * @param $section Integer: section number
+        * @param $deftext: default to return if section is not found
+        * @return string text of the requested section
+        */
+       public function getSection( $text, $section, $deftext='' ) {
+               return $this->extractSections( $text, $section, "get", $deftext );
+       }
+
+       public function replaceSection( $oldtext, $section, $text ) {
+               return $this->extractSections( $oldtext, $section, "replace", $text );
+       }
+
+       /**
+        * Get the timestamp associated with the current revision, adjusted for
+        * the default server-local timestamp
+        */
+       function getRevisionTimestamp() {
+               if ( is_null( $this->mRevisionTimestamp ) ) {
+                       wfProfileIn( __METHOD__ );
+                       global $wgContLang;
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
+                                       array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
+
+                       // Normalize timestamp to internal MW format for timezone processing.
+                       // This has the added side-effect of replacing a null value with
+                       // the current time, which gives us more sensible behavior for
+                       // previews.
+                       $timestamp = wfTimestamp( TS_MW, $timestamp );
+
+                       // The cryptic '' timezone parameter tells to use the site-default
+                       // timezone offset instead of the user settings.
+                       //
+                       // Since this value will be saved into the parser cache, served
+                       // to other users, and potentially even used inside links and such,
+                       // it needs to be consistent for all visitors.
+                       $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
+
+                       wfProfileOut( __METHOD__ );
+               }
+               return $this->mRevisionTimestamp;
+       }
+
+       /**
+        * Mutator for $mDefaultSort
+        *
+        * @param $sort New value
+        */
+       public function setDefaultSort( $sort ) {
+               $this->mDefaultSort = $sort;
+       }
+
+       /**
+        * Accessor for $mDefaultSort
+        * Will use the title/prefixed title if none is set
+        *
+        * @return string
+        */
+       public function getDefaultSort() {
+               if( $this->mDefaultSort !== false ) {
+                       return $this->mDefaultSort;
+               } else {
+                       return $this->mTitle->getNamespace() == NS_CATEGORY
+                                       ? $this->mTitle->getText()
+                                       : $this->mTitle->getPrefixedText();
+               }
+       }
+
+       /**
+        * Try to guess the section anchor name based on a wikitext fragment
+        * presumably extracted from a heading, for example "Header" from
+        * "== Header ==".
+        */
+       public function guessSectionNameFromWikiText( $text ) {
+               # Strip out wikitext links(they break the anchor)
+               $text = $this->stripSectionName( $text );
+               $headline = Sanitizer::decodeCharReferences( $text );
+               # strip out HTML
+               $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
+               $headline = trim( $headline );
+               $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
+               $replacearray = array(
+                       '%3A' => ':',
+                       '%' => '.'
+               );
+               return str_replace(
+                       array_keys( $replacearray ),
+                       array_values( $replacearray ),
+                       $sectionanchor );
+       }
+
+       /**
+        * Strips a text string of wikitext for use in a section anchor
+        *
+        * Accepts a text string and then removes all wikitext from the
+        * string and leaves only the resultant text (i.e. the result of
+        * [[User:WikiSysop|Sysop]] would be "Sysop" and the result of
+        * [[User:WikiSysop]] would be "User:WikiSysop") - this is intended
+        * to create valid section anchors by mimicing the output of the
+        * parser when headings are parsed.
+        *
+        * @param $text string Text string to be stripped of wikitext
+        * for use in a Section anchor
+        * @return Filtered text string
+        */
+       public function stripSectionName( $text ) {
+               # Strip internal link markup
+               $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
+               $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
+
+               # Strip external link markup (FIXME: Not Tolerant to blank link text
+               # I.E. [http://www.mediawiki.org] will render as [1] or something depending
+               # on how many empty links there are on the page - need to figure that out.
+               $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
+
+               # Parse wikitext quotes (italics & bold)
+               $text = $this->doQuotes($text);
+
+               # Strip HTML tags
+               $text = StringUtils::delimiterReplace( '<', '>', '', $text );
+               return $text;
+       }
+
+       /**
+        * strip/replaceVariables/unstrip for preprocessor regression testing
+        */
+       function srvus( $text ) {
+               $text = $this->strip( $text, $this->mStripState );
+               $text = Sanitizer::removeHTMLtags( $text );
+               $text = $this->replaceVariables( $text );
+               $text = preg_replace( '/<!--MWTEMPLATESECTION.*?-->/', '', $text );
+               $text = $this->mStripState->unstripBoth( $text );
+               return $text;
+       }
+}
diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php
new file mode 100644 (file)
index 0000000..322e197
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * @ingroup Parser
+ */
+interface Preprocessor {
+       /** Create a new preprocessor object based on an initialised Parser object */
+       function __construct( $parser );
+
+       /** Create a new top-level frame for expansion of a page */
+       function newFrame();
+
+       /** Preprocess text to a PPNode */
+       function preprocessToObj( $text, $flags = 0 );
+}
+
+/**
+ * @ingroup Parser
+ */
+interface PPFrame {
+       const NO_ARGS = 1;
+       const NO_TEMPLATES = 2;
+       const STRIP_COMMENTS = 4;
+       const NO_IGNORE = 8;
+       const RECOVER_COMMENTS = 16;
+
+       const RECOVER_ORIG = 27; // = 1|2|8|16 no constant expression support in PHP yet
+
+       /**
+        * Create a child frame
+        */
+       function newChild( $args = false, $title = false );
+
+       /**
+        * Expand a document tree node
+        */
+       function expand( $root, $flags = 0 );
+
+       /**
+        * Implode with flags for expand()
+        */
+       function implodeWithFlags( $sep, $flags /*, ... */ );
+
+       /**
+        * Implode with no flags specified
+        */
+       function implode( $sep /*, ... */ );
+
+       /**
+        * Makes an object that, when expand()ed, will be the same as one obtained
+        * with implode()
+        */
+       function virtualImplode( $sep /*, ... */ );
+
+       /**
+        * Virtual implode with brackets
+        */
+       function virtualBracketedImplode( $start, $sep, $end /*, ... */ );
+
+       /**
+        * Returns true if there are no arguments in this frame
+        */
+       function isEmpty();
+
+       /**
+        * Get an argument to this frame by name
+        */
+       function getArgument( $name );
+
+       /**
+        * Returns true if the infinite loop check is OK, false if a loop is detected
+        */
+       function loopCheck( $title );
+
+       /**
+        * Return true if the frame is a template frame
+        */
+       function isTemplate();
+}
+
+/**
+ * There are three types of nodes:
+ *     * Tree nodes, which have a name and contain other nodes as children
+ *     * Array nodes, which also contain other nodes but aren't considered part of a tree
+ *     * Leaf nodes, which contain the actual data
+ *
+ * This interface provides access to the tree structure and to the contents of array nodes,
+ * but it does not provide access to the internal structure of leaf nodes. Access to leaf
+ * data is provided via two means:
+ *     * PPFrame::expand(), which provides expanded text
+ *     * The PPNode::split*() functions, which provide metadata about certain types of tree node
+ * @ingroup Parser
+ */
+interface PPNode {
+       /**
+        * Get an array-type node containing the children of this node.
+        * Returns false if this is not a tree node.
+        */
+       function getChildren();
+
+       /**
+        * Get the first child of a tree node. False if there isn't one.
+        */
+       function getFirstChild();
+
+       /**
+        * Get the next sibling of any node. False if there isn't one
+        */
+       function getNextSibling();
+
+       /**
+        * Get all children of this tree node which have a given name.
+        * Returns an array-type node, or false if this is not a tree node.
+        */
+       function getChildrenOfType( $type );
+
+
+       /**
+        * Returns the length of the array, or false if this is not an array-type node
+        */
+       function getLength();
+
+       /**
+        * Returns an item of an array-type node
+        */
+       function item( $i );
+
+       /**
+        * Get the name of this node. The following names are defined here:
+        *
+        *    h             A heading node.
+        *    template      A double-brace node.
+        *    tplarg        A triple-brace node.
+        *    title         The first argument to a template or tplarg node.
+        *    part          Subsequent arguments to a template or tplarg node.
+        *    #nodelist     An array-type node
+        *
+        * The subclass may define various other names for tree and leaf nodes.
+        */
+       function getName();
+
+       /**
+        * Split a <part> node into an associative array containing:
+        *    name          PPNode name
+        *    index         String index
+        *    value         PPNode value
+        */
+       function splitArg();
+
+       /**
+        * Split an <ext> node into an associative array containing name, attr, inner and close
+        * All values in the resulting array are PPNodes. Inner and close are optional.
+        */
+       function splitExt();
+
+       /**
+        * Split an <h> node
+        */
+       function splitHeading();
+}
diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php
new file mode 100644 (file)
index 0000000..01776cd
--- /dev/null
@@ -0,0 +1,1379 @@
+<?php
+
+/**
+ * @ingroup Parser
+ */
+class Preprocessor_DOM implements Preprocessor {
+       var $parser, $memoryLimit;
+
+       function __construct( $parser ) {
+               $this->parser = $parser;
+               $mem = ini_get( 'memory_limit' );
+               $this->memoryLimit = false;
+               if ( strval( $mem ) !== '' && $mem != -1 ) {
+                       if ( preg_match( '/^\d+$/', $mem ) ) {
+                               $this->memoryLimit = $mem;
+                       } elseif ( preg_match( '/^(\d+)M$/i', $mem, $m ) ) {
+                               $this->memoryLimit = $m[1] * 1048576;
+                       }
+               }
+       }
+
+       function newFrame() {
+               return new PPFrame_DOM( $this );
+       }
+
+       function memCheck() {
+               if ( $this->memoryLimit === false ) {
+                       return;
+               }
+               $usage = memory_get_usage();
+               if ( $usage > $this->memoryLimit * 0.9 ) {
+                       $limit = intval( $this->memoryLimit * 0.9 / 1048576 + 0.5 );
+                       throw new MWException( "Preprocessor hit 90% memory limit ($limit MB)" );
+               }
+               return $usage <= $this->memoryLimit * 0.8;
+       }
+
+       /**
+        * Preprocess some wikitext and return the document tree.
+        * This is the ghost of Parser::replace_variables().
+        *
+        * @param string $text The text to parse
+        * @param integer flags Bitwise combination of:
+        *          Parser::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
+        *                                     included. Default is to assume a direct page view.
+        *
+        * The generated DOM tree must depend only on the input text and the flags.
+        * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+        *
+        * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+        * change in the DOM tree for a given text, must be passed through the section identifier
+        * in the section edit link and thus back to extractSections().
+        *
+        * The output of this function is currently only cached in process memory, but a persistent
+        * cache may be implemented at a later date which takes further advantage of these strict
+        * dependency requirements.
+        *
+        * @private
+        */
+       function preprocessToObj( $text, $flags = 0 ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__.'-makexml' );
+
+               $rules = array(
+                       '{' => array(
+                               'end' => '}',
+                               'names' => array(
+                                       2 => 'template',
+                                       3 => 'tplarg',
+                               ),
+                               'min' => 2,
+                               'max' => 3,
+                       ),
+                       '[' => array(
+                               'end' => ']',
+                               'names' => array( 2 => null ),
+                               'min' => 2,
+                               'max' => 2,
+                       )
+               );
+
+               $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
+
+               $xmlishElements = $this->parser->getStripList();
+               $enableOnlyinclude = false;
+               if ( $forInclusion ) {
+                       $ignoredTags = array( 'includeonly', '/includeonly' );
+                       $ignoredElements = array( 'noinclude' );
+                       $xmlishElements[] = 'noinclude';
+                       if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
+                               $enableOnlyinclude = true;
+                       }
+               } else {
+                       $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
+                       $ignoredElements = array( 'includeonly' );
+                       $xmlishElements[] = 'includeonly';
+               }
+               $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
+
+               // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
+               $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
+
+               $stack = new PPDStack;
+
+               $searchBase = "[{<\n"; #}
+               $revText = strrev( $text ); // For fast reverse searches
+
+               $i = 0;                     # Input pointer, starts out pointing to a pseudo-newline before the start
+               $accum =& $stack->getAccum();   # Current accumulator
+               $accum = '<root>';
+               $findEquals = false;            # True to find equals signs in arguments
+               $findPipe = false;              # True to take notice of pipe characters
+               $headingIndex = 1;
+               $inHeading = false;        # True if $i is inside a possible heading
+               $noMoreGT = false;         # True if there are no more greater-than (>) signs right of $i
+               $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
+               $fakeLineStart = true;     # Do a line-start run without outputting an LF character
+
+               while ( true ) {
+                       //$this->memCheck();
+
+                       if ( $findOnlyinclude ) {
+                               // Ignore all input up to the next <onlyinclude>
+                               $startPos = strpos( $text, '<onlyinclude>', $i );
+                               if ( $startPos === false ) {
+                                       // Ignored section runs to the end
+                                       $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i ) ) . '</ignore>';
+                                       break;
+                               }
+                               $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
+                               $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i ) ) . '</ignore>';
+                               $i = $tagEndPos;
+                               $findOnlyinclude = false;
+                       }
+
+                       if ( $fakeLineStart ) {
+                               $found = 'line-start';
+                               $curChar = '';
+                       } else {
+                               # Find next opening brace, closing brace or pipe
+                               $search = $searchBase;
+                               if ( $stack->top === false ) {
+                                       $currentClosing = '';
+                               } else {
+                                       $currentClosing = $stack->top->close;
+                                       $search .= $currentClosing;
+                               }
+                               if ( $findPipe ) {
+                                       $search .= '|';
+                               }
+                               if ( $findEquals ) {
+                                       // First equals will be for the template
+                                       $search .= '=';
+                               }
+                               $rule = null;
+                               # Output literal section, advance input counter
+                               $literalLength = strcspn( $text, $search, $i );
+                               if ( $literalLength > 0 ) {
+                                       $accum .= htmlspecialchars( substr( $text, $i, $literalLength ) );
+                                       $i += $literalLength;
+                               }
+                               if ( $i >= strlen( $text ) ) {
+                                       if ( $currentClosing == "\n" ) {
+                                               // Do a past-the-end run to finish off the heading
+                                               $curChar = '';
+                                               $found = 'line-end';
+                                       } else {
+                                               # All done
+                                               break;
+                                       }
+                               } else {
+                                       $curChar = $text[$i];
+                                       if ( $curChar == '|' ) {
+                                               $found = 'pipe';
+                                       } elseif ( $curChar == '=' ) {
+                                               $found = 'equals';
+                                       } elseif ( $curChar == '<' ) {
+                                               $found = 'angle';
+                                       } elseif ( $curChar == "\n" ) {
+                                               if ( $inHeading ) {
+                                                       $found = 'line-end';
+                                               } else {
+                                                       $found = 'line-start';
+                                               }
+                                       } elseif ( $curChar == $currentClosing ) {
+                                               $found = 'close';
+                                       } elseif ( isset( $rules[$curChar] ) ) {
+                                               $found = 'open';
+                                               $rule = $rules[$curChar];
+                                       } else {
+                                               # Some versions of PHP have a strcspn which stops on null characters
+                                               # Ignore and continue
+                                               ++$i;
+                                               continue;
+                                       }
+                               }
+                       }
+
+                       if ( $found == 'angle' ) {
+                               $matches = false;
+                               // Handle </onlyinclude>
+                               if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
+                                       $findOnlyinclude = true;
+                                       continue;
+                               }
+
+                               // Determine element name
+                               if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
+                                       // Element name missing or not listed
+                                       $accum .= '&lt;';
+                                       ++$i;
+                                       continue;
+                               }
+                               // Handle comments
+                               if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
+                                       // To avoid leaving blank lines, when a comment is both preceded
+                                       // and followed by a newline (ignoring spaces), trim leading and
+                                       // trailing spaces and one of the newlines.
+
+                                       // Find the end
+                                       $endPos = strpos( $text, '-->', $i + 4 );
+                                       if ( $endPos === false ) {
+                                               // Unclosed comment in input, runs to end
+                                               $inner = substr( $text, $i );
+                                               $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+                                               $i = strlen( $text );
+                                       } else {
+                                               // Search backwards for leading whitespace
+                                               $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+                                               // Search forwards for trailing whitespace
+                                               // $wsEnd will be the position of the last space
+                                               $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+                                               // Eat the line if possible
+                                               // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
+                                               // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
+                                               // it's a possible beneficial b/c break.
+                                               if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
+                                                       && substr( $text, $wsEnd + 1, 1 ) == "\n" )
+                                               {
+                                                       $startPos = $wsStart;
+                                                       $endPos = $wsEnd + 1;
+                                                       // Remove leading whitespace from the end of the accumulator
+                                                       // Sanity check first though
+                                                       $wsLength = $i - $wsStart;
+                                                       if ( $wsLength > 0 && substr( $accum, -$wsLength ) === str_repeat( ' ', $wsLength ) ) {
+                                                               $accum = substr( $accum, 0, -$wsLength );
+                                                       }
+                                                       // Do a line-start run next time to look for headings after the comment
+                                                       $fakeLineStart = true;
+                                               } else {
+                                                       // No line to eat, just take the comment itself
+                                                       $startPos = $i;
+                                                       $endPos += 2;
+                                               }
+
+                                               if ( $stack->top ) {
+                                                       $part = $stack->top->getCurrentPart();
+                                                       if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
+                                                               // Comments abutting, no change in visual end
+                                                               $part->commentEnd = $wsEnd;
+                                                       } else {
+                                                               $part->visualEnd = $wsStart;
+                                                               $part->commentEnd = $endPos;
+                                                       }
+                                               }
+                                               $i = $endPos + 1;
+                                               $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
+                                               $accum .= '<comment>' . htmlspecialchars( $inner ) . '</comment>';
+                                       }
+                                       continue;
+                               }
+                               $name = $matches[1];
+                               $lowerName = strtolower( $name );
+                               $attrStart = $i + strlen( $name ) + 1;
+
+                               // Find end of tag
+                               $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
+                               if ( $tagEndPos === false ) {
+                                       // Infinite backtrack
+                                       // Disable tag search to prevent worst-case O(N^2) performance
+                                       $noMoreGT = true;
+                                       $accum .= '&lt;';
+                                       ++$i;
+                                       continue;
+                               }
+
+                               // Handle ignored tags
+                               if ( in_array( $lowerName, $ignoredTags ) ) {
+                                       $accum .= '<ignore>' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . '</ignore>';
+                                       $i = $tagEndPos + 1;
+                                       continue;
+                               }
+
+                               $tagStartPos = $i;
+                               if ( $text[$tagEndPos-1] == '/' ) {
+                                       $attrEnd = $tagEndPos - 1;
+                                       $inner = null;
+                                       $i = $tagEndPos + 1;
+                                       $close = '';
+                               } else {
+                                       $attrEnd = $tagEndPos;
+                                       // Find closing tag
+                                       if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
+                                               $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
+                                               $i = $matches[0][1] + strlen( $matches[0][0] );
+                                               $close = '<close>' . htmlspecialchars( $matches[0][0] ) . '</close>';
+                                       } else {
+                                               // No end tag -- let it run out to the end of the text.
+                                               $inner = substr( $text, $tagEndPos + 1 );
+                                               $i = strlen( $text );
+                                               $close = '';
+                                       }
+                               }
+                               // <includeonly> and <noinclude> just become <ignore> tags
+                               if ( in_array( $lowerName, $ignoredElements ) ) {
+                                       $accum .= '<ignore>' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) )
+                                               . '</ignore>';
+                                       continue;
+                               }
+
+                               $accum .= '<ext>';
+                               if ( $attrEnd <= $attrStart ) {
+                                       $attr = '';
+                               } else {
+                                       $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
+                               }
+                               $accum .= '<name>' . htmlspecialchars( $name ) . '</name>' .
+                                       // Note that the attr element contains the whitespace between name and attribute,
+                                       // this is necessary for precise reconstruction during pre-save transform.
+                                       '<attr>' . htmlspecialchars( $attr ) . '</attr>';
+                               if ( $inner !== null ) {
+                                       $accum .= '<inner>' . htmlspecialchars( $inner ) . '</inner>';
+                               }
+                               $accum .= $close . '</ext>';
+                       }
+
+                       elseif ( $found == 'line-start' ) {
+                               // Is this the start of a heading?
+                               // Line break belongs before the heading element in any case
+                               if ( $fakeLineStart ) {
+                                       $fakeLineStart = false;
+                               } else {
+                                       $accum .= $curChar;
+                                       $i++;
+                               }
+
+                               $count = strspn( $text, '=', $i, 6 );
+                               if ( $count == 1 && $findEquals ) {
+                                       // DWIM: This looks kind of like a name/value separator
+                                       // Let's let the equals handler have it and break the potential heading
+                                       // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
+                               } elseif ( $count > 0 ) {
+                                       $piece = array(
+                                               'open' => "\n",
+                                               'close' => "\n",
+                                               'parts' => array( new PPDPart( str_repeat( '=', $count ) ) ),
+                                               'startPos' => $i,
+                                               'count' => $count );
+                                       $stack->push( $piece );
+                                       $accum =& $stack->getAccum();
+                                       extract( $stack->getFlags() );
+                                       $i += $count;
+                               }
+                       }
+
+                       elseif ( $found == 'line-end' ) {
+                               $piece = $stack->top;
+                               // A heading must be open, otherwise \n wouldn't have been in the search list
+                               assert( $piece->open == "\n" );
+                               $part = $piece->getCurrentPart();
+                               // Search back through the input to see if it has a proper close
+                               // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
+                               $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+                               $searchStart = $i - $wsLength;
+                               if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
+                                       // Comment found at line end
+                                       // Search for equals signs before the comment
+                                       $searchStart = $part->visualEnd;
+                                       $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+                               }
+                               $count = $piece->count;
+                               $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+                               if ( $equalsLength > 0 ) {
+                                       if ( $i - $equalsLength == $piece->startPos ) {
+                                               // This is just a single string of equals signs on its own line
+                                               // Replicate the doHeadings behaviour /={count}(.+)={count}/
+                                               // First find out how many equals signs there really are (don't stop at 6)
+                                               $count = $equalsLength;
+                                               if ( $count < 3 ) {
+                                                       $count = 0;
+                                               } else {
+                                                       $count = min( 6, intval( ( $count - 1 ) / 2 ) );
+                                               }
+                                       } else {
+                                               $count = min( $equalsLength, $count );
+                                       }
+                                       if ( $count > 0 ) {
+                                               // Normal match, output <h>
+                                               $element = "<h level=\"$count\" i=\"$headingIndex\">$accum</h>";
+                                               $headingIndex++;
+                                       } else {
+                                               // Single equals sign on its own line, count=0
+                                               $element = $accum;
+                                       }
+                               } else {
+                                       // No match, no <h>, just pass down the inner text
+                                       $element = $accum;
+                               }
+                               // Unwind the stack
+                               $stack->pop();
+                               $accum =& $stack->getAccum();
+                               extract( $stack->getFlags() );
+
+                               // Append the result to the enclosing accumulator
+                               $accum .= $element;
+                               // Note that we do NOT increment the input pointer.
+                               // This is because the closing linebreak could be the opening linebreak of
+                               // another heading. Infinite loops are avoided because the next iteration MUST
+                               // hit the heading open case above, which unconditionally increments the
+                               // input pointer.
+                       }
+
+                       elseif ( $found == 'open' ) {
+                               # count opening brace characters
+                               $count = strspn( $text, $curChar, $i );
+
+                               # we need to add to stack only if opening brace count is enough for one of the rules
+                               if ( $count >= $rule['min'] ) {
+                                       # Add it to the stack
+                                       $piece = array(
+                                               'open' => $curChar,
+                                               'close' => $rule['end'],
+                                               'count' => $count,
+                                               'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+                                       );
+
+                                       $stack->push( $piece );
+                                       $accum =& $stack->getAccum();
+                                       extract( $stack->getFlags() );
+                               } else {
+                                       # Add literal brace(s)
+                                       $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+                               }
+                               $i += $count;
+                       }
+
+                       elseif ( $found == 'close' ) {
+                               $piece = $stack->top;
+                               # lets check if there are enough characters for closing brace
+                               $maxCount = $piece->count;
+                               $count = strspn( $text, $curChar, $i, $maxCount );
+
+                               # check for maximum matching characters (if there are 5 closing
+                               # characters, we will probably need only 3 - depending on the rules)
+                               $matchingCount = 0;
+                               $rule = $rules[$piece->open];
+                               if ( $count > $rule['max'] ) {
+                                       # The specified maximum exists in the callback array, unless the caller
+                                       # has made an error
+                                       $matchingCount = $rule['max'];
+                               } else {
+                                       # Count is less than the maximum
+                                       # Skip any gaps in the callback array to find the true largest match
+                                       # Need to use array_key_exists not isset because the callback can be null
+                                       $matchingCount = $count;
+                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
+                                               --$matchingCount;
+                                       }
+                               }
+
+                               if ($matchingCount <= 0) {
+                                       # No matching element found in callback array
+                                       # Output a literal closing brace and continue
+                                       $accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
+                                       $i += $count;
+                                       continue;
+                               }
+                               $name = $rule['names'][$matchingCount];
+                               if ( $name === null ) {
+                                       // No element, just literal text
+                                       $element = $piece->breakSyntax( $matchingCount ) . str_repeat( $rule['end'], $matchingCount );
+                               } else {
+                                       # Create XML element
+                                       # Note: $parts is already XML, does not need to be encoded further
+                                       $parts = $piece->parts;
+                                       $title = $parts[0]->out;
+                                       unset( $parts[0] );
+
+                                       # The invocation is at the start of the line if lineStart is set in
+                                       # the stack, and all opening brackets are used up.
+                                       if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+                                               $attr = ' lineStart="1"';
+                                       } else {
+                                               $attr = '';
+                                       }
+
+                                       $element = "<$name$attr>";
+                                       $element .= "<title>$title</title>";
+                                       $argIndex = 1;
+                                       foreach ( $parts as $partIndex => $part ) {
+                                               if ( isset( $part->eqpos ) ) {
+                                                       $argName = substr( $part->out, 0, $part->eqpos );
+                                                       $argValue = substr( $part->out, $part->eqpos + 1 );
+                                                       $element .= "<part><name>$argName</name>=<value>$argValue</value></part>";
+                                               } else {
+                                                       $element .= "<part><name index=\"$argIndex\" /><value>{$part->out}</value></part>";
+                                                       $argIndex++;
+                                               }
+                                       }
+                                       $element .= "</$name>";
+                               }
+
+                               # Advance input pointer
+                               $i += $matchingCount;
+
+                               # Unwind the stack
+                               $stack->pop();
+                               $accum =& $stack->getAccum();
+
+                               # Re-add the old stack element if it still has unmatched opening characters remaining
+                               if ($matchingCount < $piece->count) {
+                                       $piece->parts = array( new PPDPart );
+                                       $piece->count -= $matchingCount;
+                                       # do we still qualify for any callback with remaining count?
+                                       $names = $rules[$piece->open]['names'];
+                                       $skippedBraces = 0;
+                                       $enclosingAccum =& $accum;
+                                       while ( $piece->count ) {
+                                               if ( array_key_exists( $piece->count, $names ) ) {
+                                                       $stack->push( $piece );
+                                                       $accum =& $stack->getAccum();
+                                                       break;
+                                               }
+                                               --$piece->count;
+                                               $skippedBraces ++;
+                                       }
+                                       $enclosingAccum .= str_repeat( $piece->open, $skippedBraces );
+                               }
+
+                               extract( $stack->getFlags() );
+
+                               # Add XML element to the enclosing accumulator
+                               $accum .= $element;
+                       }
+
+                       elseif ( $found == 'pipe' ) {
+                               $findEquals = true; // shortcut for getFlags()
+                               $stack->addPart();
+                               $accum =& $stack->getAccum();
+                               ++$i;
+                       }
+
+                       elseif ( $found == 'equals' ) {
+                               $findEquals = false; // shortcut for getFlags()
+                               $stack->getCurrentPart()->eqpos = strlen( $accum );
+                               $accum .= '=';
+                               ++$i;
+                       }
+               }
+
+               # Output any remaining unclosed brackets
+               foreach ( $stack->stack as $piece ) {
+                       $stack->rootAccum .= $piece->breakSyntax();
+               }
+               $stack->rootAccum .= '</root>';
+               $xml = $stack->rootAccum;
+
+               wfProfileOut( __METHOD__.'-makexml' );
+               wfProfileIn( __METHOD__.'-loadXML' );
+               $dom = new DOMDocument;
+               wfSuppressWarnings();
+               $result = $dom->loadXML( $xml );
+               wfRestoreWarnings();
+               if ( !$result ) {
+                       // Try running the XML through UtfNormal to get rid of invalid characters
+                       $xml = UtfNormal::cleanUp( $xml );
+                       $result = $dom->loadXML( $xml );
+                       if ( !$result ) {
+                               throw new MWException( __METHOD__.' generated invalid XML' );
+                       }
+               }
+               $obj = new PPNode_DOM( $dom->documentElement );
+               wfProfileOut( __METHOD__.'-loadXML' );
+               wfProfileOut( __METHOD__ );
+               return $obj;
+       }
+}
+
+/**
+ * Stack class to help Preprocessor::preprocessToObj()
+ * @ingroup Parser
+ */
+class PPDStack {
+       var $stack, $rootAccum, $top;
+       var $out;
+       var $elementClass = 'PPDStackElement';
+
+       static $false = false;
+
+       function __construct() {
+               $this->stack = array();
+               $this->top = false;
+               $this->rootAccum = '';
+               $this->accum =& $this->rootAccum;
+       }
+
+       function count() {
+               return count( $this->stack );
+       }
+
+       function &getAccum() {
+               return $this->accum;
+       }
+
+       function getCurrentPart() {
+               if ( $this->top === false ) {
+                       return false;
+               } else {
+                       return $this->top->getCurrentPart();
+               }
+       }
+
+       function push( $data ) {
+               if ( $data instanceof $this->elementClass ) {
+                       $this->stack[] = $data;
+               } else {
+                       $class = $this->elementClass;
+                       $this->stack[] = new $class( $data );
+               }
+               $this->top = $this->stack[ count( $this->stack ) - 1 ];
+               $this->accum =& $this->top->getAccum();
+       }
+
+       function pop() {
+               if ( !count( $this->stack ) ) {
+                       throw new MWException( __METHOD__.': no elements remaining' );
+               }
+               $temp = array_pop( $this->stack );
+
+               if ( count( $this->stack ) ) {
+                       $this->top = $this->stack[ count( $this->stack ) - 1 ];
+                       $this->accum =& $this->top->getAccum();
+               } else {
+                       $this->top = self::$false;
+                       $this->accum =& $this->rootAccum;
+               }
+               return $temp;
+       }
+
+       function addPart( $s = '' ) {
+               $this->top->addPart( $s );
+               $this->accum =& $this->top->getAccum();
+       }
+
+       function getFlags() {
+               if ( !count( $this->stack ) ) {
+                       return array(
+                               'findEquals' => false,
+                               'findPipe' => false,
+                               'inHeading' => false,
+                       );
+               } else {
+                       return $this->top->getFlags();
+               }
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDStackElement {
+       var $open,                      // Opening character (\n for heading)
+               $close,             // Matching closing character
+               $count,             // Number of opening characters found (number of "=" for heading)
+               $parts,             // Array of PPDPart objects describing pipe-separated parts.
+               $lineStart;         // True if the open char appeared at the start of the input line. Not set for headings.
+
+       var $partClass = 'PPDPart';
+
+       function __construct( $data = array() ) {
+               $class = $this->partClass;
+               $this->parts = array( new $class );
+
+               foreach ( $data as $name => $value ) {
+                       $this->$name = $value;
+               }
+       }
+
+       function &getAccum() {
+               return $this->parts[count($this->parts) - 1]->out;
+       }
+
+       function addPart( $s = '' ) {
+               $class = $this->partClass;
+               $this->parts[] = new $class( $s );
+       }
+
+       function getCurrentPart() {
+               return $this->parts[count($this->parts) - 1];
+       }
+
+       function getFlags() {
+               $partCount = count( $this->parts );
+               $findPipe = $this->open != "\n" && $this->open != '[';
+               return array(
+                       'findPipe' => $findPipe,
+                       'findEquals' => $findPipe && $partCount > 1 && !isset( $this->parts[$partCount - 1]->eqpos ),
+                       'inHeading' => $this->open == "\n",
+               );
+       }
+
+       /**
+        * Get the output string that would result if the close is not found.
+        */
+       function breakSyntax( $openingCount = false ) {
+               if ( $this->open == "\n" ) {
+                       $s = $this->parts[0]->out;
+               } else {
+                       if ( $openingCount === false ) {
+                               $openingCount = $this->count;
+                       }
+                       $s = str_repeat( $this->open, $openingCount );
+                       $first = true;
+                       foreach ( $this->parts as $part ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $s .= '|';
+                               }
+                               $s .= $part->out;
+                       }
+               }
+               return $s;
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDPart {
+       var $out; // Output accumulator string
+
+       // Optional member variables:
+       //   eqpos        Position of equals sign in output accumulator
+       //   commentEnd   Past-the-end input pointer for the last comment encountered
+       //   visualEnd    Past-the-end input pointer for the end of the accumulator minus comments
+
+       function __construct( $out = '' ) {
+               $this->out = $out;
+       }
+}
+
+/**
+ * An expansion frame, used as a context to expand the result of preprocessToObj()
+ * @ingroup Parser
+ */
+class PPFrame_DOM implements PPFrame {
+       var $preprocessor, $parser, $title;
+       var $titleCache;
+
+       /**
+        * Hashtable listing templates which are disallowed for expansion in this frame,
+        * having been encountered previously in parent frames.
+        */
+       var $loopCheckHash;
+
+       /**
+        * Recursion depth of this frame, top = 0
+        */
+       var $depth;
+
+
+       /**
+        * Construct a new preprocessor frame.
+        * @param Preprocessor $preprocessor The parent preprocessor
+        */
+       function __construct( $preprocessor ) {
+               $this->preprocessor = $preprocessor;
+               $this->parser = $preprocessor->parser;
+               $this->title = $this->parser->mTitle;
+               $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
+               $this->loopCheckHash = array();
+               $this->depth = 0;
+       }
+
+       /**
+        * Create a new child frame
+        * $args is optionally a multi-root PPNode or array containing the template arguments
+        */
+       function newChild( $args = false, $title = false ) {
+               $namedArgs = array();
+               $numberedArgs = array();
+               if ( $title === false ) {
+                       $title = $this->title;
+               }
+               if ( $args !== false ) {
+                       $xpath = false;
+                       if ( $args instanceof PPNode ) {
+                               $args = $args->node;
+                       }
+                       foreach ( $args as $arg ) {
+                               if ( !$xpath ) {
+                                       $xpath = new DOMXPath( $arg->ownerDocument );
+                               }
+
+                               $nameNodes = $xpath->query( 'name', $arg );
+                               $value = $xpath->query( 'value', $arg );
+                               if ( $nameNodes->item( 0 )->hasAttributes() ) {
+                                       // Numbered parameter
+                                       $index = $nameNodes->item( 0 )->attributes->getNamedItem( 'index' )->textContent;
+                                       $numberedArgs[$index] = $value->item( 0 );
+                                       unset( $namedArgs[$index] );
+                               } else {
+                                       // Named parameter
+                                       $name = trim( $this->expand( $nameNodes->item( 0 ), PPFrame::STRIP_COMMENTS ) );
+                                       $namedArgs[$name] = $value->item( 0 );
+                                       unset( $numberedArgs[$name] );
+                               }
+                       }
+               }
+               return new PPTemplateFrame_DOM( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
+       }
+
+       function expand( $root, $flags = 0 ) {
+               static $depth = 0;
+               if ( is_string( $root ) ) {
+                       return $root;
+               }
+
+               if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
+               {
+                       return '<span class="error">Node-count limit exceeded</span>';
+               }
+
+               if ( $depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
+                       return '<span class="error">Expansion depth limit exceeded</span>';
+               }
+               ++$depth;
+
+               if ( $root instanceof PPNode_DOM ) {
+                       $root = $root->node;
+               }
+               if ( $root instanceof DOMDocument ) {
+                       $root = $root->documentElement;
+               }
+
+               $outStack = array( '', '' );
+               $iteratorStack = array( false, $root );
+               $indexStack = array( 0, 0 );
+
+               while ( count( $iteratorStack ) > 1 ) {
+                       $level = count( $outStack ) - 1;
+                       $iteratorNode =& $iteratorStack[ $level ];
+                       $out =& $outStack[$level];
+                       $index =& $indexStack[$level];
+
+                       if ( $iteratorNode instanceof PPNode_DOM ) $iteratorNode = $iteratorNode->node;
+
+                       if ( is_array( $iteratorNode ) ) {
+                               if ( $index >= count( $iteratorNode ) ) {
+                                       // All done with this iterator
+                                       $iteratorStack[$level] = false;
+                                       $contextNode = false;
+                               } else {
+                                       $contextNode = $iteratorNode[$index];
+                                       $index++;
+                               }
+                       } elseif ( $iteratorNode instanceof DOMNodeList ) {
+                               if ( $index >= $iteratorNode->length ) {
+                                       // All done with this iterator
+                                       $iteratorStack[$level] = false;
+                                       $contextNode = false;
+                               } else {
+                                       $contextNode = $iteratorNode->item( $index );
+                                       $index++;
+                               }
+                       } else {
+                               // Copy to $contextNode and then delete from iterator stack,
+                               // because this is not an iterator but we do have to execute it once
+                               $contextNode = $iteratorStack[$level];
+                               $iteratorStack[$level] = false;
+                       }
+
+                       if ( $contextNode instanceof PPNode_DOM ) $contextNode = $contextNode->node;
+
+                       $newIterator = false;
+
+                       if ( $contextNode === false ) {
+                               // nothing to do
+                       } elseif ( is_string( $contextNode ) ) {
+                               $out .= $contextNode;
+                       } elseif ( is_array( $contextNode ) || $contextNode instanceof DOMNodeList ) {
+                               $newIterator = $contextNode;
+                       } elseif ( $contextNode instanceof DOMNode ) {
+                               if ( $contextNode->nodeType == XML_TEXT_NODE ) {
+                                       $out .= $contextNode->nodeValue;
+                               } elseif ( $contextNode->nodeName == 'template' ) {
+                                       # Double-brace expansion
+                                       $xpath = new DOMXPath( $contextNode->ownerDocument );
+                                       $titles = $xpath->query( 'title', $contextNode );
+                                       $title = $titles->item( 0 );
+                                       $parts = $xpath->query( 'part', $contextNode );
+                                       if ( $flags & self::NO_TEMPLATES ) {
+                                               $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $title, $parts );
+                                       } else {
+                                               $lineStart = $contextNode->getAttribute( 'lineStart' );
+                                               $params = array(
+                                                       'title' => new PPNode_DOM( $title ),
+                                                       'parts' => new PPNode_DOM( $parts ),
+                                                       'lineStart' => $lineStart );
+                                               $ret = $this->parser->braceSubstitution( $params, $this );
+                                               if ( isset( $ret['object'] ) ) {
+                                                       $newIterator = $ret['object'];
+                                               } else {
+                                                       $out .= $ret['text'];
+                                               }
+                                       }
+                               } elseif ( $contextNode->nodeName == 'tplarg' ) {
+                                       # Triple-brace expansion
+                                       $xpath = new DOMXPath( $contextNode->ownerDocument );
+                                       $titles = $xpath->query( 'title', $contextNode );
+                                       $title = $titles->item( 0 );
+                                       $parts = $xpath->query( 'part', $contextNode );
+                                       if ( $flags & self::NO_ARGS ) {
+                                               $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $title, $parts );
+                                       } else {
+                                               $params = array(
+                                                       'title' => new PPNode_DOM( $title ),
+                                                       'parts' => new PPNode_DOM( $parts ) );
+                                               $ret = $this->parser->argSubstitution( $params, $this );
+                                               if ( isset( $ret['object'] ) ) {
+                                                       $newIterator = $ret['object'];
+                                               } else {
+                                                       $out .= $ret['text'];
+                                               }
+                                       }
+                               } elseif ( $contextNode->nodeName == 'comment' ) {
+                                       # HTML-style comment
+                                       # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
+                                       if ( $this->parser->ot['html']
+                                               || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
+                                               || ( $flags & self::STRIP_COMMENTS ) )
+                                       {
+                                               $out .= '';
+                                       }
+                                       # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
+                                       # Not in RECOVER_COMMENTS mode (extractSections) though
+                                       elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
+                                               $out .= $this->parser->insertStripItem( $contextNode->textContent );
+                                       }
+                                       # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
+                                       else {
+                                               $out .= $contextNode->textContent;
+                                       }
+                               } elseif ( $contextNode->nodeName == 'ignore' ) {
+                                       # Output suppression used by <includeonly> etc.
+                                       # OT_WIKI will only respect <ignore> in substed templates.
+                                       # The other output types respect it unless NO_IGNORE is set.
+                                       # extractSections() sets NO_IGNORE and so never respects it.
+                                       if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
+                                               $out .= $contextNode->textContent;
+                                       } else {
+                                               $out .= '';
+                                       }
+                               } elseif ( $contextNode->nodeName == 'ext' ) {
+                                       # Extension tag
+                                       $xpath = new DOMXPath( $contextNode->ownerDocument );
+                                       $names = $xpath->query( 'name', $contextNode );
+                                       $attrs = $xpath->query( 'attr', $contextNode );
+                                       $inners = $xpath->query( 'inner', $contextNode );
+                                       $closes = $xpath->query( 'close', $contextNode );
+                                       $params = array(
+                                               'name' => new PPNode_DOM( $names->item( 0 ) ),
+                                               'attr' => $attrs->length > 0 ? new PPNode_DOM( $attrs->item( 0 ) ) : null,
+                                               'inner' => $inners->length > 0 ? new PPNode_DOM( $inners->item( 0 ) ) : null,
+                                               'close' => $closes->length > 0 ? new PPNode_DOM( $closes->item( 0 ) ) : null,
+                                       );
+                                       $out .= $this->parser->extensionSubstitution( $params, $this );
+                               } elseif ( $contextNode->nodeName == 'h' ) {
+                                       # Heading
+                                       $s = $this->expand( $contextNode->childNodes, $flags );
+
+                    # Insert a heading marker only for <h> children of <root>
+                    # This is to stop extractSections from going over multiple tree levels
+                    if ( $contextNode->parentNode->nodeName == 'root'
+                      && $this->parser->ot['html'] )
+                    {
+                                               # Insert heading index marker
+                                               $headingIndex = $contextNode->getAttribute( 'i' );
+                                               $titleText = $this->title->getPrefixedDBkey();
+                                               $this->parser->mHeadings[] = array( $titleText, $headingIndex );
+                                               $serial = count( $this->parser->mHeadings ) - 1;
+                                               $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
+                                               $count = $contextNode->getAttribute( 'level' );
+                                               $s = substr( $s, 0, $count ) . $marker . substr( $s, $count );
+                                               $this->parser->mStripState->general->setPair( $marker, '' );
+                                       }
+                                       $out .= $s;
+                               } else {
+                                       # Generic recursive expansion
+                                       $newIterator = $contextNode->childNodes;
+                               }
+                       } else {
+                               throw new MWException( __METHOD__.': Invalid parameter type' );
+                       }
+
+                       if ( $newIterator !== false ) {
+                               if ( $newIterator instanceof PPNode_DOM ) {
+                                       $newIterator = $newIterator->node;
+                               }
+                               $outStack[] = '';
+                               $iteratorStack[] = $newIterator;
+                               $indexStack[] = 0;
+                       } elseif ( $iteratorStack[$level] === false ) {
+                               // Return accumulated value to parent
+                               // With tail recursion
+                               while ( $iteratorStack[$level] === false && $level > 0 ) {
+                                       $outStack[$level - 1] .= $out;
+                                       array_pop( $outStack );
+                                       array_pop( $iteratorStack );
+                                       array_pop( $indexStack );
+                                       $level--;
+                               }
+                       }
+               }
+               --$depth;
+               return $outStack[0];
+       }
+
+       function implodeWithFlags( $sep, $flags /*, ... */ ) {
+               $args = array_slice( func_get_args(), 2 );
+
+               $first = true;
+               $s = '';
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_DOM ) $root = $root->node;
+                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $s .= $sep;
+                               }
+                               $s .= $this->expand( $node, $flags );
+                       }
+               }
+               return $s;
+       }
+
+       /**
+        * Implode with no flags specified
+        * This previously called implodeWithFlags but has now been inlined to reduce stack depth
+        */
+       function implode( $sep /*, ... */ ) {
+               $args = array_slice( func_get_args(), 1 );
+
+               $first = true;
+               $s = '';
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_DOM ) $root = $root->node;
+                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $s .= $sep;
+                               }
+                               $s .= $this->expand( $node );
+                       }
+               }
+               return $s;
+       }
+
+       /**
+        * Makes an object that, when expand()ed, will be the same as one obtained
+        * with implode()
+        */
+       function virtualImplode( $sep /*, ... */ ) {
+               $args = array_slice( func_get_args(), 1 );
+               $out = array();
+               $first = true;
+               if ( $root instanceof PPNode_DOM ) $root = $root->node;
+
+               foreach ( $args as $root ) {
+                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $out[] = $sep;
+                               }
+                               $out[] = $node;
+                       }
+               }
+               return $out;
+       }
+
+       /**
+        * Virtual implode with brackets
+        */
+       function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
+               $args = array_slice( func_get_args(), 3 );
+               $out = array( $start );
+               $first = true;
+
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_DOM ) $root = $root->node;
+                       if ( !is_array( $root ) && !( $root instanceof DOMNodeList ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $out[] = $sep;
+                               }
+                               $out[] = $node;
+                       }
+               }
+               $out[] = $end;
+               return $out;
+       }
+
+       function __toString() {
+               return 'frame{}';
+       }
+
+       function getPDBK( $level = false ) {
+               if ( $level === false ) {
+                       return $this->title->getPrefixedDBkey();
+               } else {
+                       return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
+               }
+       }
+
+       /**
+        * Returns true if there are no arguments in this frame
+        */
+       function isEmpty() {
+               return true;
+       }
+
+       function getArgument( $name ) {
+               return false;
+       }
+
+       /**
+        * Returns true if the infinite loop check is OK, false if a loop is detected
+        */
+       function loopCheck( $title ) {
+               return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
+       }
+
+       /**
+        * Return true if the frame is a template frame
+        */
+       function isTemplate() {
+               return false;
+       }
+}
+
+/**
+ * Expansion frame with template arguments
+ * @ingroup Parser
+ */
+class PPTemplateFrame_DOM extends PPFrame_DOM {
+       var $numberedArgs, $namedArgs, $parent;
+       var $numberedExpansionCache, $namedExpansionCache;
+
+       function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
+               $this->preprocessor = $preprocessor;
+               $this->parser = $preprocessor->parser;
+               $this->parent = $parent;
+               $this->numberedArgs = $numberedArgs;
+               $this->namedArgs = $namedArgs;
+               $this->title = $title;
+               $pdbk = $title ? $title->getPrefixedDBkey() : false;
+               $this->titleCache = $parent->titleCache;
+               $this->titleCache[] = $pdbk;
+               $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
+               if ( $pdbk !== false ) {
+                       $this->loopCheckHash[$pdbk] = true;
+               }
+               $this->depth = $parent->depth + 1;
+               $this->numberedExpansionCache = $this->namedExpansionCache = array();
+       }
+
+       function __toString() {
+               $s = 'tplframe{';
+               $first = true;
+               $args = $this->numberedArgs + $this->namedArgs;
+               foreach ( $args as $name => $value ) {
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $s .= ', ';
+                       }
+                       $s .= "\"$name\":\"" .
+                               str_replace( '"', '\\"', $value->ownerDocument->saveXML( $value ) ) . '"';
+               }
+               $s .= '}';
+               return $s;
+       }
+       /**
+        * Returns true if there are no arguments in this frame
+        */
+       function isEmpty() {
+               return !count( $this->numberedArgs ) && !count( $this->namedArgs );
+       }
+
+       function getNumberedArgument( $index ) {
+               if ( !isset( $this->numberedArgs[$index] ) ) {
+                       return false;
+               }
+               if ( !isset( $this->numberedExpansionCache[$index] ) ) {
+                       # No trimming for unnamed arguments
+                       $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
+               }
+               return $this->numberedExpansionCache[$index];
+       }
+
+       function getNamedArgument( $name ) {
+               if ( !isset( $this->namedArgs[$name] ) ) {
+                       return false;
+               }
+               if ( !isset( $this->namedExpansionCache[$name] ) ) {
+                       # Trim named arguments post-expand, for backwards compatibility
+                       $this->namedExpansionCache[$name] = trim(
+                               $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
+               }
+               return $this->namedExpansionCache[$name];
+       }
+
+       function getArgument( $name ) {
+               $text = $this->getNumberedArgument( $name );
+               if ( $text === false ) {
+                       $text = $this->getNamedArgument( $name );
+               }
+               return $text;
+       }
+
+       /**
+        * Return true if the frame is a template frame
+        */
+       function isTemplate() {
+               return true;
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_DOM implements PPNode {
+       var $node;
+
+       function __construct( $node, $xpath = false ) {
+               $this->node = $node;
+       }
+
+       function __get( $name ) {
+               if ( $name == 'xpath' ) {
+                       $this->xpath = new DOMXPath( $this->node->ownerDocument );
+               }
+               return $this->xpath;
+       }
+
+       function __toString() {
+               if ( $this->node instanceof DOMNodeList ) {
+                       $s = '';
+                       foreach ( $this->node as $node ) {
+                               $s .= $node->ownerDocument->saveXML( $node );
+                       }
+               } else {
+                       $s = $this->node->ownerDocument->saveXML( $this->node );
+               }
+               return $s;
+       }
+
+       function getChildren() {
+               return $this->node->childNodes ? new self( $this->node->childNodes ) : false;
+       }
+
+       function getFirstChild() {
+               return $this->node->firstChild ? new self( $this->node->firstChild ) : false;
+       }
+
+       function getNextSibling() {
+               return $this->node->nextSibling ? new self( $this->node->nextSibling ) : false;
+       }
+
+       function getChildrenOfType( $type ) {
+               return new self( $this->xpath->query( $type, $this->node ) );
+       }
+
+       function getLength() {
+               if ( $this->node instanceof DOMNodeList ) {
+                       return $this->node->length;
+               } else {
+                       return false;
+               }
+       }
+
+       function item( $i ) {
+               $item = $this->node->item( $i );
+               return $item ? new self( $item ) : false;
+       }
+
+       function getName() {
+               if ( $this->node instanceof DOMNodeList ) {
+                       return '#nodelist';
+               } else {
+                       return $this->node->nodeName;
+               }
+       }
+
+       /**
+        * Split a <part> node into an associative array containing:
+        *    name          PPNode name
+        *    index         String index
+        *    value         PPNode value
+        */
+       function splitArg() {
+               $names = $this->xpath->query( 'name', $this->node );
+               $values = $this->xpath->query( 'value', $this->node );
+               if ( !$names->length || !$values->length ) {
+                       throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
+               }
+               $name = $names->item( 0 );
+               $index = $name->getAttribute( 'index' );
+               return array(
+                       'name' => new self( $name ),
+                       'index' => $index,
+                       'value' => new self( $values->item( 0 ) ) );
+       }
+
+       /**
+        * Split an <ext> node into an associative array containing name, attr, inner and close
+        * All values in the resulting array are PPNodes. Inner and close are optional.
+        */
+       function splitExt() {
+               $names = $this->xpath->query( 'name', $this->node );
+               $attrs = $this->xpath->query( 'attr', $this->node );
+               $inners = $this->xpath->query( 'inner', $this->node );
+               $closes = $this->xpath->query( 'close', $this->node );
+               if ( !$names->length || !$attrs->length ) {
+                       throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
+               }
+               $parts = array(
+                       'name' => new self( $names->item( 0 ) ),
+                       'attr' => new self( $attrs->item( 0 ) ) );
+               if ( $inners->length ) {
+                       $parts['inner'] = new self( $inners->item( 0 ) );
+               }
+               if ( $closes->length ) {
+                       $parts['close'] = new self( $closes->item( 0 ) );
+               }
+               return $parts;
+       }
+
+       /**
+        * Split a <h> node
+        */
+       function splitHeading() {
+               if ( !$this->nodeName == 'h' ) {
+                       throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+               }
+               return array(
+                       'i' => $this->node->getAttribute( 'i' ),
+                       'level' => $this->node->getAttribute( 'level' ),
+                       'contents' => $this->getChildren()
+               );
+       }
+}
diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php
new file mode 100644 (file)
index 0000000..dc33788
--- /dev/null
@@ -0,0 +1,1497 @@
+<?php
+
+/**
+ * Differences from DOM schema:
+ *   * attribute nodes are children
+ *   * <h> nodes that aren't at the top are replaced with <possible-h>
+ * @ingroup Parser
+ */
+class Preprocessor_Hash implements Preprocessor {
+       var $parser;
+
+       function __construct( $parser ) {
+               $this->parser = $parser;
+       }
+
+       function newFrame() {
+               return new PPFrame_Hash( $this );
+       }
+
+       /**
+        * Preprocess some wikitext and return the document tree.
+        * This is the ghost of Parser::replace_variables().
+        *
+        * @param string $text The text to parse
+        * @param integer flags Bitwise combination of:
+        *          Parser::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
+        *                                     included. Default is to assume a direct page view.
+        *
+        * The generated DOM tree must depend only on the input text and the flags.
+        * The DOM tree must be the same in OT_HTML and OT_WIKI mode, to avoid a regression of bug 4899.
+        *
+        * Any flag added to the $flags parameter here, or any other parameter liable to cause a
+        * change in the DOM tree for a given text, must be passed through the section identifier
+        * in the section edit link and thus back to extractSections().
+        *
+        * The output of this function is currently only cached in process memory, but a persistent
+        * cache may be implemented at a later date which takes further advantage of these strict
+        * dependency requirements.
+        *
+        * @private
+        */
+       function preprocessToObj( $text, $flags = 0 ) {
+               wfProfileIn( __METHOD__ );
+
+               $rules = array(
+                       '{' => array(
+                               'end' => '}',
+                               'names' => array(
+                                       2 => 'template',
+                                       3 => 'tplarg',
+                               ),
+                               'min' => 2,
+                               'max' => 3,
+                       ),
+                       '[' => array(
+                               'end' => ']',
+                               'names' => array( 2 => null ),
+                               'min' => 2,
+                               'max' => 2,
+                       )
+               );
+
+               $forInclusion = $flags & Parser::PTD_FOR_INCLUSION;
+
+               $xmlishElements = $this->parser->getStripList();
+               $enableOnlyinclude = false;
+               if ( $forInclusion ) {
+                       $ignoredTags = array( 'includeonly', '/includeonly' );
+                       $ignoredElements = array( 'noinclude' );
+                       $xmlishElements[] = 'noinclude';
+                       if ( strpos( $text, '<onlyinclude>' ) !== false && strpos( $text, '</onlyinclude>' ) !== false ) {
+                               $enableOnlyinclude = true;
+                       }
+               } else {
+                       $ignoredTags = array( 'noinclude', '/noinclude', 'onlyinclude', '/onlyinclude' );
+                       $ignoredElements = array( 'includeonly' );
+                       $xmlishElements[] = 'includeonly';
+               }
+               $xmlishRegex = implode( '|', array_merge( $xmlishElements, $ignoredTags ) );
+
+               // Use "A" modifier (anchored) instead of "^", because ^ doesn't work with an offset
+               $elementsRegex = "~($xmlishRegex)(?:\s|\/>|>)|(!--)~iA";
+
+               $stack = new PPDStack_Hash;
+
+               $searchBase = "[{<\n";
+               $revText = strrev( $text ); // For fast reverse searches
+
+               $i = 0;                     # Input pointer, starts out pointing to a pseudo-newline before the start
+               $accum =& $stack->getAccum();   # Current accumulator
+               $findEquals = false;            # True to find equals signs in arguments
+               $findPipe = false;              # True to take notice of pipe characters
+               $headingIndex = 1;
+               $inHeading = false;        # True if $i is inside a possible heading
+               $noMoreGT = false;         # True if there are no more greater-than (>) signs right of $i
+               $findOnlyinclude = $enableOnlyinclude; # True to ignore all input up to the next <onlyinclude>
+               $fakeLineStart = true;     # Do a line-start run without outputting an LF character
+
+               while ( true ) {
+                       //$this->memCheck();
+
+                       if ( $findOnlyinclude ) {
+                               // Ignore all input up to the next <onlyinclude>
+                               $startPos = strpos( $text, '<onlyinclude>', $i );
+                               if ( $startPos === false ) {
+                                       // Ignored section runs to the end
+                                       $accum->addNodeWithText( 'ignore', substr( $text, $i ) );
+                                       break;
+                               }
+                               $tagEndPos = $startPos + strlen( '<onlyinclude>' ); // past-the-end
+                               $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i ) );
+                               $i = $tagEndPos;
+                               $findOnlyinclude = false;
+                       }
+
+                       if ( $fakeLineStart ) {
+                               $found = 'line-start';
+                               $curChar = '';
+                       } else {
+                               # Find next opening brace, closing brace or pipe
+                               $search = $searchBase;
+                               if ( $stack->top === false ) {
+                                       $currentClosing = '';
+                               } else {
+                                       $currentClosing = $stack->top->close;
+                                       $search .= $currentClosing;
+                               }
+                               if ( $findPipe ) {
+                                       $search .= '|';
+                               }
+                               if ( $findEquals ) {
+                                       // First equals will be for the template
+                                       $search .= '=';
+                               }
+                               $rule = null;
+                               # Output literal section, advance input counter
+                               $literalLength = strcspn( $text, $search, $i );
+                               if ( $literalLength > 0 ) {
+                                       $accum->addLiteral( substr( $text, $i, $literalLength ) );
+                                       $i += $literalLength;
+                               }
+                               if ( $i >= strlen( $text ) ) {
+                                       if ( $currentClosing == "\n" ) {
+                                               // Do a past-the-end run to finish off the heading
+                                               $curChar = '';
+                                               $found = 'line-end';
+                                       } else {
+                                               # All done
+                                               break;
+                                       }
+                               } else {
+                                       $curChar = $text[$i];
+                                       if ( $curChar == '|' ) {
+                                               $found = 'pipe';
+                                       } elseif ( $curChar == '=' ) {
+                                               $found = 'equals';
+                                       } elseif ( $curChar == '<' ) {
+                                               $found = 'angle';
+                                       } elseif ( $curChar == "\n" ) {
+                                               if ( $inHeading ) {
+                                                       $found = 'line-end';
+                                               } else {
+                                                       $found = 'line-start';
+                                               }
+                                       } elseif ( $curChar == $currentClosing ) {
+                                               $found = 'close';
+                                       } elseif ( isset( $rules[$curChar] ) ) {
+                                               $found = 'open';
+                                               $rule = $rules[$curChar];
+                                       } else {
+                                               # Some versions of PHP have a strcspn which stops on null characters
+                                               # Ignore and continue
+                                               ++$i;
+                                               continue;
+                                       }
+                               }
+                       }
+
+                       if ( $found == 'angle' ) {
+                               $matches = false;
+                               // Handle </onlyinclude>
+                               if ( $enableOnlyinclude && substr( $text, $i, strlen( '</onlyinclude>' ) ) == '</onlyinclude>' ) {
+                                       $findOnlyinclude = true;
+                                       continue;
+                               }
+
+                               // Determine element name
+                               if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) {
+                                       // Element name missing or not listed
+                                       $accum->addLiteral( '<' );
+                                       ++$i;
+                                       continue;
+                               }
+                               // Handle comments
+                               if ( isset( $matches[2] ) && $matches[2] == '!--' ) {
+                                       // To avoid leaving blank lines, when a comment is both preceded
+                                       // and followed by a newline (ignoring spaces), trim leading and
+                                       // trailing spaces and one of the newlines.
+
+                                       // Find the end
+                                       $endPos = strpos( $text, '-->', $i + 4 );
+                                       if ( $endPos === false ) {
+                                               // Unclosed comment in input, runs to end
+                                               $inner = substr( $text, $i );
+                                               $accum->addNodeWithText( 'comment', $inner );
+                                               $i = strlen( $text );
+                                       } else {
+                                               // Search backwards for leading whitespace
+                                               $wsStart = $i ? ( $i - strspn( $revText, ' ', strlen( $text ) - $i ) ) : 0;
+                                               // Search forwards for trailing whitespace
+                                               // $wsEnd will be the position of the last space
+                                               $wsEnd = $endPos + 2 + strspn( $text, ' ', $endPos + 3 );
+                                               // Eat the line if possible
+                                               // TODO: This could theoretically be done if $wsStart == 0, i.e. for comments at
+                                               // the overall start. That's not how Sanitizer::removeHTMLcomments() did it, but
+                                               // it's a possible beneficial b/c break.
+                                               if ( $wsStart > 0 && substr( $text, $wsStart - 1, 1 ) == "\n"
+                                                       && substr( $text, $wsEnd + 1, 1 ) == "\n" )
+                                               {
+                                                       $startPos = $wsStart;
+                                                       $endPos = $wsEnd + 1;
+                                                       // Remove leading whitespace from the end of the accumulator
+                                                       // Sanity check first though
+                                                       $wsLength = $i - $wsStart;
+                                                       if ( $wsLength > 0
+                                                               && $accum->lastNode instanceof PPNode_Hash_Text
+                                                               && substr( $accum->lastNode->value, -$wsLength ) === str_repeat( ' ', $wsLength ) )
+                                                       {
+                                                               $accum->lastNode->value = substr( $accum->lastNode->value, 0, -$wsLength );
+                                                       }
+                                                       // Do a line-start run next time to look for headings after the comment
+                                                       $fakeLineStart = true;
+                                               } else {
+                                                       // No line to eat, just take the comment itself
+                                                       $startPos = $i;
+                                                       $endPos += 2;
+                                               }
+
+                                               if ( $stack->top ) {
+                                                       $part = $stack->top->getCurrentPart();
+                                                       if ( isset( $part->commentEnd ) && $part->commentEnd == $wsStart - 1 ) {
+                                                               // Comments abutting, no change in visual end
+                                                               $part->commentEnd = $wsEnd;
+                                                       } else {
+                                                               $part->visualEnd = $wsStart;
+                                                               $part->commentEnd = $endPos;
+                                                       }
+                                               }
+                                               $i = $endPos + 1;
+                                               $inner = substr( $text, $startPos, $endPos - $startPos + 1 );
+                                               $accum->addNodeWithText( 'comment', $inner );
+                                       }
+                                       continue;
+                               }
+                               $name = $matches[1];
+                               $lowerName = strtolower( $name );
+                               $attrStart = $i + strlen( $name ) + 1;
+
+                               // Find end of tag
+                               $tagEndPos = $noMoreGT ? false : strpos( $text, '>', $attrStart );
+                               if ( $tagEndPos === false ) {
+                                       // Infinite backtrack
+                                       // Disable tag search to prevent worst-case O(N^2) performance
+                                       $noMoreGT = true;
+                                       $accum->addLiteral( '<' );
+                                       ++$i;
+                                       continue;
+                               }
+
+                               // Handle ignored tags
+                               if ( in_array( $lowerName, $ignoredTags ) ) {
+                                       $accum->addNodeWithText( 'ignore', substr( $text, $i, $tagEndPos - $i + 1 ) );
+                                       $i = $tagEndPos + 1;
+                                       continue;
+                               }
+
+                               $tagStartPos = $i;
+                               if ( $text[$tagEndPos-1] == '/' ) {
+                                       // Short end tag
+                                       $attrEnd = $tagEndPos - 1;
+                                       $inner = null;
+                                       $i = $tagEndPos + 1;
+                                       $close = null;
+                               } else {
+                                       $attrEnd = $tagEndPos;
+                                       // Find closing tag
+                                       if ( preg_match( "/<\/$name\s*>/i", $text, $matches, PREG_OFFSET_CAPTURE, $tagEndPos + 1 ) ) {
+                                               $inner = substr( $text, $tagEndPos + 1, $matches[0][1] - $tagEndPos - 1 );
+                                               $i = $matches[0][1] + strlen( $matches[0][0] );
+                                               $close = $matches[0][0];
+                                       } else {
+                                               // No end tag -- let it run out to the end of the text.
+                                               $inner = substr( $text, $tagEndPos + 1 );
+                                               $i = strlen( $text );
+                                               $close = null;
+                                       }
+                               }
+                               // <includeonly> and <noinclude> just become <ignore> tags
+                               if ( in_array( $lowerName, $ignoredElements ) ) {
+                                       $accum->addNodeWithText(  'ignore', substr( $text, $tagStartPos, $i - $tagStartPos ) );
+                                       continue;
+                               }
+
+                               if ( $attrEnd <= $attrStart ) {
+                                       $attr = '';
+                               } else {
+                                       // Note that the attr element contains the whitespace between name and attribute,
+                                       // this is necessary for precise reconstruction during pre-save transform.
+                                       $attr = substr( $text, $attrStart, $attrEnd - $attrStart );
+                               }
+
+                               $extNode = new PPNode_Hash_Tree( 'ext' );
+                               $extNode->addChild( PPNode_Hash_Tree::newWithText( 'name', $name ) );
+                               $extNode->addChild( PPNode_Hash_Tree::newWithText( 'attr', $attr ) );
+                               if ( $inner !== null ) {
+                                       $extNode->addChild( PPNode_Hash_Tree::newWithText( 'inner', $inner ) );
+                               }
+                               if ( $close !== null ) {
+                                       $extNode->addChild( PPNode_Hash_Tree::newWithText( 'close', $close ) );
+                               }
+                               $accum->addNode( $extNode );
+                       }
+
+                       elseif ( $found == 'line-start' ) {
+                               // Is this the start of a heading?
+                               // Line break belongs before the heading element in any case
+                               if ( $fakeLineStart ) {
+                                       $fakeLineStart = false;
+                               } else {
+                                       $accum->addLiteral( $curChar );
+                                       $i++;
+                               }
+
+                               $count = strspn( $text, '=', $i, 6 );
+                               if ( $count == 1 && $findEquals ) {
+                                       // DWIM: This looks kind of like a name/value separator
+                                       // Let's let the equals handler have it and break the potential heading
+                                       // This is heuristic, but AFAICT the methods for completely correct disambiguation are very complex.
+                               } elseif ( $count > 0 ) {
+                                       $piece = array(
+                                               'open' => "\n",
+                                               'close' => "\n",
+                                               'parts' => array( new PPDPart_Hash( str_repeat( '=', $count ) ) ),
+                                               'startPos' => $i,
+                                               'count' => $count );
+                                       $stack->push( $piece );
+                                       $accum =& $stack->getAccum();
+                                       extract( $stack->getFlags() );
+                                       $i += $count;
+                               }
+                       }
+
+                       elseif ( $found == 'line-end' ) {
+                               $piece = $stack->top;
+                               // A heading must be open, otherwise \n wouldn't have been in the search list
+                               assert( $piece->open == "\n" );
+                               $part = $piece->getCurrentPart();
+                               // Search back through the input to see if it has a proper close
+                               // Do this using the reversed string since the other solutions (end anchor, etc.) are inefficient
+                               $wsLength = strspn( $revText, " \t", strlen( $text ) - $i );
+                               $searchStart = $i - $wsLength;
+                               if ( isset( $part->commentEnd ) && $searchStart - 1 == $part->commentEnd ) {
+                                       // Comment found at line end
+                                       // Search for equals signs before the comment
+                                       $searchStart = $part->visualEnd;
+                                       $searchStart -= strspn( $revText, " \t", strlen( $text ) - $searchStart );
+                               }
+                               $count = $piece->count;
+                               $equalsLength = strspn( $revText, '=', strlen( $text ) - $searchStart );
+                               if ( $equalsLength > 0 ) {
+                                       if ( $i - $equalsLength == $piece->startPos ) {
+                                               // This is just a single string of equals signs on its own line
+                                               // Replicate the doHeadings behaviour /={count}(.+)={count}/
+                                               // First find out how many equals signs there really are (don't stop at 6)
+                                               $count = $equalsLength;
+                                               if ( $count < 3 ) {
+                                                       $count = 0;
+                                               } else {
+                                                       $count = min( 6, intval( ( $count - 1 ) / 2 ) );
+                                               }
+                                       } else {
+                                               $count = min( $equalsLength, $count );
+                                       }
+                                       if ( $count > 0 ) {
+                                               // Normal match, output <h>
+                                               $element = new PPNode_Hash_Tree( 'possible-h' );
+                                               $element->addChild( new PPNode_Hash_Attr( 'level', $count ) );
+                                               $element->addChild( new PPNode_Hash_Attr( 'i', $headingIndex++ ) );
+                                               $element->lastChild->nextSibling = $accum->firstNode;
+                                               $element->lastChild = $accum->lastNode;
+                                       } else {
+                                               // Single equals sign on its own line, count=0
+                                               $element = $accum;
+                                       }
+                               } else {
+                                       // No match, no <h>, just pass down the inner text
+                                       $element = $accum;
+                               }
+                               // Unwind the stack
+                               $stack->pop();
+                               $accum =& $stack->getAccum();
+                               extract( $stack->getFlags() );
+
+                               // Append the result to the enclosing accumulator
+                               if ( $element instanceof PPNode ) {
+                                       $accum->addNode( $element );
+                               } else {
+                                       $accum->addAccum( $element );
+                               }
+                               // Note that we do NOT increment the input pointer.
+                               // This is because the closing linebreak could be the opening linebreak of
+                               // another heading. Infinite loops are avoided because the next iteration MUST
+                               // hit the heading open case above, which unconditionally increments the
+                               // input pointer.
+                       }
+
+                       elseif ( $found == 'open' ) {
+                               # count opening brace characters
+                               $count = strspn( $text, $curChar, $i );
+
+                               # we need to add to stack only if opening brace count is enough for one of the rules
+                               if ( $count >= $rule['min'] ) {
+                                       # Add it to the stack
+                                       $piece = array(
+                                               'open' => $curChar,
+                                               'close' => $rule['end'],
+                                               'count' => $count,
+                                               'lineStart' => ($i > 0 && $text[$i-1] == "\n"),
+                                       );
+
+                                       $stack->push( $piece );
+                                       $accum =& $stack->getAccum();
+                                       extract( $stack->getFlags() );
+                               } else {
+                                       # Add literal brace(s)
+                                       $accum->addLiteral( str_repeat( $curChar, $count ) );
+                               }
+                               $i += $count;
+                       }
+
+                       elseif ( $found == 'close' ) {
+                               $piece = $stack->top;
+                               # lets check if there are enough characters for closing brace
+                               $maxCount = $piece->count;
+                               $count = strspn( $text, $curChar, $i, $maxCount );
+
+                               # check for maximum matching characters (if there are 5 closing
+                               # characters, we will probably need only 3 - depending on the rules)
+                               $matchingCount = 0;
+                               $rule = $rules[$piece->open];
+                               if ( $count > $rule['max'] ) {
+                                       # The specified maximum exists in the callback array, unless the caller
+                                       # has made an error
+                                       $matchingCount = $rule['max'];
+                               } else {
+                                       # Count is less than the maximum
+                                       # Skip any gaps in the callback array to find the true largest match
+                                       # Need to use array_key_exists not isset because the callback can be null
+                                       $matchingCount = $count;
+                                       while ( $matchingCount > 0 && !array_key_exists( $matchingCount, $rule['names'] ) ) {
+                                               --$matchingCount;
+                                       }
+                               }
+
+                               if ($matchingCount <= 0) {
+                                       # No matching element found in callback array
+                                       # Output a literal closing brace and continue
+                                       $accum->addLiteral( str_repeat( $curChar, $count ) );
+                                       $i += $count;
+                                       continue;
+                               }
+                               $name = $rule['names'][$matchingCount];
+                               if ( $name === null ) {
+                                       // No element, just literal text
+                                       $element = $piece->breakSyntax( $matchingCount );
+                                       $element->addLiteral( str_repeat( $rule['end'], $matchingCount ) );
+                               } else {
+                                       # Create XML element
+                                       # Note: $parts is already XML, does not need to be encoded further
+                                       $parts = $piece->parts;
+                                       $titleAccum = $parts[0]->out;
+                                       unset( $parts[0] );
+
+                                       $element = new PPNode_Hash_Tree( $name );
+
+                                       # The invocation is at the start of the line if lineStart is set in
+                                       # the stack, and all opening brackets are used up.
+                                       if ( $maxCount == $matchingCount && !empty( $piece->lineStart ) ) {
+                                               $element->addChild( new PPNode_Hash_Attr( 'lineStart', 1 ) );
+                                       }
+                                       $titleNode = new PPNode_Hash_Tree( 'title' );
+                                       $titleNode->firstChild = $titleAccum->firstNode;
+                                       $titleNode->lastChild = $titleAccum->lastNode;
+                                       $element->addChild( $titleNode );
+                                       $argIndex = 1;
+                                       foreach ( $parts as $partIndex => $part ) {
+                                               if ( isset( $part->eqpos ) ) {
+                                                       // Find equals
+                                                       $lastNode = false;
+                                                       for ( $node = $part->out->firstNode; $node; $node = $node->nextSibling ) {
+                                                               if ( $node === $part->eqpos ) {
+                                                                       break;
+                                                               }
+                                                               $lastNode = $node;
+                                                       }
+                                                       if ( !$node ) {
+                                                               throw new MWException( __METHOD__. ': eqpos not found' );
+                                                       }
+                                                       if ( $node->name !== 'equals' ) {
+                                                               throw new MWException( __METHOD__ .': eqpos is not equals' );
+                                                       }
+                                                       $equalsNode = $node;
+
+                                                       // Construct name node
+                                                       $nameNode = new PPNode_Hash_Tree( 'name' );
+                                                       if ( $lastNode !== false ) {
+                                                               $lastNode->nextSibling = false;
+                                                               $nameNode->firstChild = $part->out->firstNode;
+                                                               $nameNode->lastChild = $lastNode;
+                                                       }
+
+                                                       // Construct value node
+                                                       $valueNode = new PPNode_Hash_Tree( 'value' );
+                                                       if ( $equalsNode->nextSibling !== false ) {
+                                                               $valueNode->firstChild = $equalsNode->nextSibling;
+                                                               $valueNode->lastChild = $part->out->lastNode;
+                                                       }
+                                                       $partNode = new PPNode_Hash_Tree( 'part' );
+                                                       $partNode->addChild( $nameNode );
+                                                       $partNode->addChild( $equalsNode->firstChild );
+                                                       $partNode->addChild( $valueNode );
+                                                       $element->addChild( $partNode );
+                                               } else {
+                                                       $partNode = new PPNode_Hash_Tree( 'part' );
+                                                       $nameNode = new PPNode_Hash_Tree( 'name' );
+                                                       $nameNode->addChild( new PPNode_Hash_Attr( 'index', $argIndex++ ) );
+                                                       $valueNode = new PPNode_Hash_Tree( 'value' );
+                                                       $valueNode->firstChild = $part->out->firstNode;
+                                                       $valueNode->lastChild = $part->out->lastNode;
+                                                       $partNode->addChild( $nameNode );
+                                                       $partNode->addChild( $valueNode );
+                                                       $element->addChild( $partNode );
+                                               }
+                                       }
+                               }
+
+                               # Advance input pointer
+                               $i += $matchingCount;
+
+                               # Unwind the stack
+                               $stack->pop();
+                               $accum =& $stack->getAccum();
+
+                               # Re-add the old stack element if it still has unmatched opening characters remaining
+                               if ($matchingCount < $piece->count) {
+                                       $piece->parts = array( new PPDPart_Hash );
+                                       $piece->count -= $matchingCount;
+                                       # do we still qualify for any callback with remaining count?
+                                       $names = $rules[$piece->open]['names'];
+                                       $skippedBraces = 0;
+                                       $enclosingAccum =& $accum;
+                                       while ( $piece->count ) {
+                                               if ( array_key_exists( $piece->count, $names ) ) {
+                                                       $stack->push( $piece );
+                                                       $accum =& $stack->getAccum();
+                                                       break;
+                                               }
+                                               --$piece->count;
+                                               $skippedBraces ++;
+                                       }
+                                       $enclosingAccum->addLiteral( str_repeat( $piece->open, $skippedBraces ) );
+                               }
+
+                               extract( $stack->getFlags() );
+
+                               # Add XML element to the enclosing accumulator
+                               if ( $element instanceof PPNode ) {
+                                       $accum->addNode( $element );
+                               } else {
+                                       $accum->addAccum( $element );
+                               }
+                       }
+
+                       elseif ( $found == 'pipe' ) {
+                               $findEquals = true; // shortcut for getFlags()
+                               $stack->addPart();
+                               $accum =& $stack->getAccum();
+                               ++$i;
+                       }
+
+                       elseif ( $found == 'equals' ) {
+                               $findEquals = false; // shortcut for getFlags()
+                               $accum->addNodeWithText( 'equals', '=' );
+                               $stack->getCurrentPart()->eqpos = $accum->lastNode;
+                               ++$i;
+                       }
+               }
+
+               # Output any remaining unclosed brackets
+               foreach ( $stack->stack as $piece ) {
+                       $stack->rootAccum->addAccum( $piece->breakSyntax() );
+               }
+
+               # Enable top-level headings
+               for ( $node = $stack->rootAccum->firstNode; $node; $node = $node->nextSibling ) {
+                       if ( isset( $node->name ) && $node->name === 'possible-h' ) {
+                               $node->name = 'h';
+                       }
+               }
+
+               $rootNode = new PPNode_Hash_Tree( 'root' );
+               $rootNode->firstChild = $stack->rootAccum->firstNode;
+               $rootNode->lastChild = $stack->rootAccum->lastNode;
+               wfProfileOut( __METHOD__ );
+               return $rootNode;
+       }
+}
+
+/**
+ * Stack class to help Preprocessor::preprocessToObj()
+ * @ingroup Parser
+ */
+class PPDStack_Hash extends PPDStack {
+       function __construct() {
+               $this->elementClass = 'PPDStackElement_Hash';
+               parent::__construct();
+               $this->rootAccum = new PPDAccum_Hash;
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDStackElement_Hash extends PPDStackElement {
+       function __construct( $data = array() ) {
+               $this->partClass = 'PPDPart_Hash';
+               parent::__construct( $data );
+       }
+
+       /**
+        * Get the accumulator that would result if the close is not found.
+        */
+       function breakSyntax( $openingCount = false ) {
+               if ( $this->open == "\n" ) {
+                       $accum = $this->parts[0]->out;
+               } else {
+                       if ( $openingCount === false ) {
+                               $openingCount = $this->count;
+                       }
+                       $accum = new PPDAccum_Hash;
+                       $accum->addLiteral( str_repeat( $this->open, $openingCount ) );
+                       $first = true;
+                       foreach ( $this->parts as $part ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $accum->addLiteral( '|' );
+                               }
+                               $accum->addAccum( $part->out );
+                       }
+               }
+               return $accum;
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDPart_Hash extends PPDPart {
+       function __construct( $out = '' ) {
+               $accum = new PPDAccum_Hash;
+               if ( $out !== '' ) {
+                       $accum->addLiteral( $out );
+               }
+               parent::__construct( $accum );
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPDAccum_Hash {
+       var $firstNode, $lastNode;
+
+       function __construct() {
+               $this->firstNode = $this->lastNode = false;
+       }
+
+       /**
+        * Append a string literal
+        */
+       function addLiteral( $s ) {
+               if ( $this->lastNode === false ) {
+                       $this->firstNode = $this->lastNode = new PPNode_Hash_Text( $s );
+               } elseif ( $this->lastNode instanceof PPNode_Hash_Text ) {
+                       $this->lastNode->value .= $s;
+               } else {
+                       $this->lastNode->nextSibling = new PPNode_Hash_Text( $s );
+                       $this->lastNode = $this->lastNode->nextSibling;
+               }
+       }
+
+       /**
+        * Append a PPNode
+        */
+       function addNode( PPNode $node ) {
+               if ( $this->lastNode === false ) {
+                       $this->firstNode = $this->lastNode = $node;
+               } else {
+                       $this->lastNode->nextSibling = $node;
+                       $this->lastNode = $node;
+               }
+       }
+
+       /**
+        * Append a tree node with text contents
+        */
+       function addNodeWithText( $name, $value ) {
+               $node = PPNode_Hash_Tree::newWithText( $name, $value );
+               $this->addNode( $node );
+       }
+
+       /**
+        * Append a PPAccum_Hash
+        * Takes over ownership of the nodes in the source argument. These nodes may
+        * subsequently be modified, especially nextSibling.
+        */
+       function addAccum( $accum ) {
+               if ( $accum->lastNode === false ) {
+                       // nothing to add
+               } elseif ( $this->lastNode === false ) {
+                       $this->firstNode = $accum->firstNode;
+                       $this->lastNode = $accum->lastNode;
+               } else {
+                       $this->lastNode->nextSibling = $accum->firstNode;
+                       $this->lastNode = $accum->lastNode;
+               }
+       }
+}
+
+/**
+ * An expansion frame, used as a context to expand the result of preprocessToObj()
+ * @ingroup Parser
+ */
+class PPFrame_Hash implements PPFrame {
+       var $preprocessor, $parser, $title;
+       var $titleCache;
+
+       /**
+        * Hashtable listing templates which are disallowed for expansion in this frame,
+        * having been encountered previously in parent frames.
+        */
+       var $loopCheckHash;
+
+       /**
+        * Recursion depth of this frame, top = 0
+        */
+       var $depth;
+
+
+       /**
+        * Construct a new preprocessor frame.
+        * @param Preprocessor $preprocessor The parent preprocessor
+        */
+       function __construct( $preprocessor ) {
+               $this->preprocessor = $preprocessor;
+               $this->parser = $preprocessor->parser;
+               $this->title = $this->parser->mTitle;
+               $this->titleCache = array( $this->title ? $this->title->getPrefixedDBkey() : false );
+               $this->loopCheckHash = array();
+               $this->depth = 0;
+       }
+
+       /**
+        * Create a new child frame
+        * $args is optionally a multi-root PPNode or array containing the template arguments
+        */
+       function newChild( $args = false, $title = false ) {
+               $namedArgs = array();
+               $numberedArgs = array();
+               if ( $title === false ) {
+                       $title = $this->title;
+               }
+               if ( $args !== false ) {
+                       $xpath = false;
+                       if ( $args instanceof PPNode_Hash_Array ) {
+                               $args = $args->value;
+                       } elseif ( !is_array( $args ) ) {
+                               throw new MWException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
+                       }
+                       foreach ( $args as $arg ) {
+                               $bits = $arg->splitArg();
+                               if ( $bits['index'] !== '' ) {
+                                       // Numbered parameter
+                                       $numberedArgs[$bits['index']] = $bits['value'];
+                                       unset( $namedArgs[$bits['index']] );
+                               } else {
+                                       // Named parameter
+                                       $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
+                                       $namedArgs[$name] = $bits['value'];
+                                       unset( $numberedArgs[$name] );
+                               }
+                       }
+               }
+               return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
+       }
+
+       function expand( $root, $flags = 0 ) {
+               if ( is_string( $root ) ) {
+                       return $root;
+               }
+
+               if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount )
+               {
+                       return '<span class="error">Node-count limit exceeded</span>';
+               }
+               if ( $this->depth > $this->parser->mOptions->mMaxPPExpandDepth ) {
+                       return '<span class="error">Expansion depth limit exceeded</span>';
+               }
+               ++$this->depth;
+
+               $outStack = array( '', '' );
+               $iteratorStack = array( false, $root );
+               $indexStack = array( 0, 0 );
+
+               while ( count( $iteratorStack ) > 1 ) {
+                       $level = count( $outStack ) - 1;
+                       $iteratorNode =& $iteratorStack[ $level ];
+                       $out =& $outStack[$level];
+                       $index =& $indexStack[$level];
+
+                       if ( is_array( $iteratorNode ) ) {
+                               if ( $index >= count( $iteratorNode ) ) {
+                                       // All done with this iterator
+                                       $iteratorStack[$level] = false;
+                                       $contextNode = false;
+                               } else {
+                                       $contextNode = $iteratorNode[$index];
+                                       $index++;
+                               }
+                       } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
+                               if ( $index >= $iteratorNode->getLength() ) {
+                                       // All done with this iterator
+                                       $iteratorStack[$level] = false;
+                                       $contextNode = false;
+                               } else {
+                                       $contextNode = $iteratorNode->item( $index );
+                                       $index++;
+                               }
+                       } else {
+                               // Copy to $contextNode and then delete from iterator stack,
+                               // because this is not an iterator but we do have to execute it once
+                               $contextNode = $iteratorStack[$level];
+                               $iteratorStack[$level] = false;
+                       }
+
+                       $newIterator = false;
+
+                       if ( $contextNode === false ) {
+                               // nothing to do
+                       } elseif ( is_string( $contextNode ) ) {
+                               $out .= $contextNode;
+                       } elseif ( is_array( $contextNode ) || $contextNode instanceof PPNode_Hash_Array ) {
+                               $newIterator = $contextNode;
+                       } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
+                               // No output
+                       } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
+                               $out .= $contextNode->value;
+                       } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
+                               if ( $contextNode->name == 'template' ) {
+                                       # Double-brace expansion
+                                       $bits = $contextNode->splitTemplate();
+                                       if ( $flags & self::NO_TEMPLATES ) {
+                                               $newIterator = $this->virtualBracketedImplode( '{{', '|', '}}', $bits['title'], $bits['parts'] );
+                                       } else {
+                                               $ret = $this->parser->braceSubstitution( $bits, $this );
+                                               if ( isset( $ret['object'] ) ) {
+                                                       $newIterator = $ret['object'];
+                                               } else {
+                                                       $out .= $ret['text'];
+                                               }
+                                       }
+                               } elseif ( $contextNode->name == 'tplarg' ) {
+                                       # Triple-brace expansion
+                                       $bits = $contextNode->splitTemplate();
+                                       if ( $flags & self::NO_ARGS ) {
+                                               $newIterator = $this->virtualBracketedImplode( '{{{', '|', '}}}', $bits['title'], $bits['parts'] );
+                                       } else {
+                                               $ret = $this->parser->argSubstitution( $bits, $this );
+                                               if ( isset( $ret['object'] ) ) {
+                                                       $newIterator = $ret['object'];
+                                               } else {
+                                                       $out .= $ret['text'];
+                                               }
+                                       }
+                               } elseif ( $contextNode->name == 'comment' ) {
+                                       # HTML-style comment
+                                       # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
+                                       if ( $this->parser->ot['html']
+                                               || ( $this->parser->ot['pre'] && $this->parser->mOptions->getRemoveComments() )
+                                               || ( $flags & self::STRIP_COMMENTS ) )
+                                       {
+                                               $out .= '';
+                                       }
+                                       # Add a strip marker in PST mode so that pstPass2() can run some old-fashioned regexes on the result
+                                       # Not in RECOVER_COMMENTS mode (extractSections) though
+                                       elseif ( $this->parser->ot['wiki'] && ! ( $flags & self::RECOVER_COMMENTS ) ) {
+                                               $out .= $this->parser->insertStripItem( $contextNode->firstChild->value );
+                                       }
+                                       # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
+                                       else {
+                                               $out .= $contextNode->firstChild->value;
+                                       }
+                               } elseif ( $contextNode->name == 'ignore' ) {
+                                       # Output suppression used by <includeonly> etc.
+                                       # OT_WIKI will only respect <ignore> in substed templates.
+                                       # The other output types respect it unless NO_IGNORE is set.
+                                       # extractSections() sets NO_IGNORE and so never respects it.
+                                       if ( ( !isset( $this->parent ) && $this->parser->ot['wiki'] ) || ( $flags & self::NO_IGNORE ) ) {
+                                               $out .= $contextNode->firstChild->value;
+                                       } else {
+                                               //$out .= '';
+                                       }
+                               } elseif ( $contextNode->name == 'ext' ) {
+                                       # Extension tag
+                                       $bits = $contextNode->splitExt() + array( 'attr' => null, 'inner' => null, 'close' => null );
+                                       $out .= $this->parser->extensionSubstitution( $bits, $this );
+                               } elseif ( $contextNode->name == 'h' ) {
+                                       # Heading
+                                       if ( $this->parser->ot['html'] ) {
+                                               # Expand immediately and insert heading index marker
+                                               $s = '';
+                                               for ( $node = $contextNode->firstChild; $node; $node = $node->nextSibling ) {
+                                                       $s .= $this->expand( $node, $flags );
+                                               }
+
+                                               $bits = $contextNode->splitHeading();
+                                               $titleText = $this->title->getPrefixedDBkey();
+                                               $this->parser->mHeadings[] = array( $titleText, $bits['i'] );
+                                               $serial = count( $this->parser->mHeadings ) - 1;
+                                               $marker = "{$this->parser->mUniqPrefix}-h-$serial-" . Parser::MARKER_SUFFIX;
+                                               $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
+                                               $this->parser->mStripState->general->setPair( $marker, '' );
+                                               $out .= $s;
+                                       } else {
+                                               # Expand in virtual stack
+                                               $newIterator = $contextNode->getChildren();
+                                       }
+                               } else {
+                                       # Generic recursive expansion
+                                       $newIterator = $contextNode->getChildren();
+                               }
+                       } else {
+                               throw new MWException( __METHOD__.': Invalid parameter type' );
+                       }
+
+                       if ( $newIterator !== false ) {
+                               $outStack[] = '';
+                               $iteratorStack[] = $newIterator;
+                               $indexStack[] = 0;
+                       } elseif ( $iteratorStack[$level] === false ) {
+                               // Return accumulated value to parent
+                               // With tail recursion
+                               while ( $iteratorStack[$level] === false && $level > 0 ) {
+                                       $outStack[$level - 1] .= $out;
+                                       array_pop( $outStack );
+                                       array_pop( $iteratorStack );
+                                       array_pop( $indexStack );
+                                       $level--;
+                               }
+                       }
+               }
+               --$this->depth;
+               return $outStack[0];
+       }
+
+       function implodeWithFlags( $sep, $flags /*, ... */ ) {
+               $args = array_slice( func_get_args(), 2 );
+
+               $first = true;
+               $s = '';
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_Hash_Array ) {
+                               $root = $root->value;
+                       }
+                       if ( !is_array( $root ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $s .= $sep;
+                               }
+                               $s .= $this->expand( $node, $flags );
+                       }
+               }
+               return $s;
+       }
+
+       /**
+        * Implode with no flags specified
+        * This previously called implodeWithFlags but has now been inlined to reduce stack depth
+        */
+       function implode( $sep /*, ... */ ) {
+               $args = array_slice( func_get_args(), 1 );
+
+               $first = true;
+               $s = '';
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_Hash_Array ) {
+                               $root = $root->value;
+                       }
+                       if ( !is_array( $root ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $s .= $sep;
+                               }
+                               $s .= $this->expand( $node );
+                       }
+               }
+               return $s;
+       }
+
+       /**
+        * Makes an object that, when expand()ed, will be the same as one obtained
+        * with implode()
+        */
+       function virtualImplode( $sep /*, ... */ ) {
+               $args = array_slice( func_get_args(), 1 );
+               $out = array();
+               $first = true;
+
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_Hash_Array ) {
+                               $root = $root->value;
+                       }
+                       if ( !is_array( $root ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $out[] = $sep;
+                               }
+                               $out[] = $node;
+                       }
+               }
+               return new PPNode_Hash_Array( $out );
+       }
+
+       /**
+        * Virtual implode with brackets
+        */
+       function virtualBracketedImplode( $start, $sep, $end /*, ... */ ) {
+               $args = array_slice( func_get_args(), 3 );
+               $out = array( $start );
+               $first = true;
+
+               foreach ( $args as $root ) {
+                       if ( $root instanceof PPNode_Hash_Array ) {
+                               $root = $root->value;
+                       }
+                       if ( !is_array( $root ) ) {
+                               $root = array( $root );
+                       }
+                       foreach ( $root as $node ) {
+                               if ( $first ) {
+                                       $first = false;
+                               } else {
+                                       $out[] = $sep;
+                               }
+                               $out[] = $node;
+                       }
+               }
+               $out[] = $end;
+               return new PPNode_Hash_Array( $out );
+       }
+
+       function __toString() {
+               return 'frame{}';
+       }
+
+       function getPDBK( $level = false ) {
+               if ( $level === false ) {
+                       return $this->title->getPrefixedDBkey();
+               } else {
+                       return isset( $this->titleCache[$level] ) ? $this->titleCache[$level] : false;
+               }
+       }
+
+       /**
+        * Returns true if there are no arguments in this frame
+        */
+       function isEmpty() {
+               return true;
+       }
+
+       function getArgument( $name ) {
+               return false;
+       }
+
+       /**
+        * Returns true if the infinite loop check is OK, false if a loop is detected
+        */
+       function loopCheck( $title ) {
+               return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
+       }
+
+       /**
+        * Return true if the frame is a template frame
+        */
+       function isTemplate() {
+               return false;
+       }
+}
+
+/**
+ * Expansion frame with template arguments
+ * @ingroup Parser
+ */
+class PPTemplateFrame_Hash extends PPFrame_Hash {
+       var $numberedArgs, $namedArgs, $parent;
+       var $numberedExpansionCache, $namedExpansionCache;
+
+       function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
+               $this->preprocessor = $preprocessor;
+               $this->parser = $preprocessor->parser;
+               $this->parent = $parent;
+               $this->numberedArgs = $numberedArgs;
+               $this->namedArgs = $namedArgs;
+               $this->title = $title;
+               $pdbk = $title ? $title->getPrefixedDBkey() : false;
+               $this->titleCache = $parent->titleCache;
+               $this->titleCache[] = $pdbk;
+               $this->loopCheckHash = /*clone*/ $parent->loopCheckHash;
+               if ( $pdbk !== false ) {
+                       $this->loopCheckHash[$pdbk] = true;
+               }
+               $this->depth = $parent->depth + 1;
+               $this->numberedExpansionCache = $this->namedExpansionCache = array();
+       }
+
+       function __toString() {
+               $s = 'tplframe{';
+               $first = true;
+               $args = $this->numberedArgs + $this->namedArgs;
+               foreach ( $args as $name => $value ) {
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $s .= ', ';
+                       }
+                       $s .= "\"$name\":\"" .
+                               str_replace( '"', '\\"', $value->__toString() ) . '"';
+               }
+               $s .= '}';
+               return $s;
+       }
+       /**
+        * Returns true if there are no arguments in this frame
+        */
+       function isEmpty() {
+               return !count( $this->numberedArgs ) && !count( $this->namedArgs );
+       }
+
+       function getNumberedArgument( $index ) {
+               if ( !isset( $this->numberedArgs[$index] ) ) {
+                       return false;
+               }
+               if ( !isset( $this->numberedExpansionCache[$index] ) ) {
+                       # No trimming for unnamed arguments
+                       $this->numberedExpansionCache[$index] = $this->parent->expand( $this->numberedArgs[$index], self::STRIP_COMMENTS );
+               }
+               return $this->numberedExpansionCache[$index];
+       }
+
+       function getNamedArgument( $name ) {
+               if ( !isset( $this->namedArgs[$name] ) ) {
+                       return false;
+               }
+               if ( !isset( $this->namedExpansionCache[$name] ) ) {
+                       # Trim named arguments post-expand, for backwards compatibility
+                       $this->namedExpansionCache[$name] = trim(
+                               $this->parent->expand( $this->namedArgs[$name], self::STRIP_COMMENTS ) );
+               }
+               return $this->namedExpansionCache[$name];
+       }
+
+       function getArgument( $name ) {
+               $text = $this->getNumberedArgument( $name );
+               if ( $text === false ) {
+                       $text = $this->getNamedArgument( $name );
+               }
+               return $text;
+       }
+
+       /**
+        * Return true if the frame is a template frame
+        */
+       function isTemplate() {
+               return true;
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Tree implements PPNode {
+       var $name, $firstChild, $lastChild, $nextSibling;
+
+       function __construct( $name ) {
+               $this->name = $name;
+               $this->firstChild = $this->lastChild = $this->nextSibling = false;
+       }
+
+       function __toString() {
+               $inner = '';
+               $attribs = '';
+               for ( $node = $this->firstChild; $node; $node = $node->nextSibling ) {
+                       if ( $node instanceof PPNode_Hash_Attr ) {
+                               $attribs .= ' ' . $node->name . '="' . htmlspecialchars( $node->value ) . '"';
+                       } else {
+                               $inner .= $node->__toString();
+                       }
+               }
+               if ( $inner === '' ) {
+                       return "<{$this->name}$attribs/>";
+               } else {
+                       return "<{$this->name}$attribs>$inner</{$this->name}>";
+               }
+       }
+
+       static function newWithText( $name, $text ) {
+               $obj = new self( $name );
+               $obj->addChild( new PPNode_Hash_Text( $text ) );
+               return $obj;
+       }
+
+       function addChild( $node ) {
+               if ( $this->lastChild === false ) {
+                       $this->firstChild = $this->lastChild = $node;
+               } else {
+                       $this->lastChild->nextSibling = $node;
+                       $this->lastChild = $node;
+               }
+       }
+
+       function getChildren() {
+               $children = array();
+               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+                       $children[] = $child;
+               }
+               return new PPNode_Hash_Array( $children );
+       }
+
+       function getFirstChild() {
+               return $this->firstChild;
+       }
+
+       function getNextSibling() {
+               return $this->nextSibling;
+       }
+
+       function getChildrenOfType( $name ) {
+               $children = array();
+               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+                       if ( isset( $child->name ) && $child->name === $name ) {
+                               $children[] = $name;
+                       }
+               }
+               return $children;
+       }
+
+       function getLength() { return false; }
+       function item( $i ) { return false; }
+
+       function getName() {
+               return $this->name;
+       }
+
+       /**
+        * Split a <part> node into an associative array containing:
+        *    name          PPNode name
+        *    index         String index
+        *    value         PPNode value
+        */
+       function splitArg() {
+               $bits = array();
+               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+                       if ( !isset( $child->name ) ) {
+                               continue;
+                       }
+                       if ( $child->name === 'name' ) {
+                               $bits['name'] = $child;
+                               if ( $child->firstChild instanceof PPNode_Hash_Attr
+                                       && $child->firstChild->name === 'index' )
+                               {
+                                       $bits['index'] = $child->firstChild->value;
+                               }
+                       } elseif ( $child->name === 'value' ) {
+                               $bits['value'] = $child;
+                       }
+               }
+
+               if ( !isset( $bits['name'] ) ) {
+                       throw new MWException( 'Invalid brace node passed to ' . __METHOD__ );
+               }
+               if ( !isset( $bits['index'] ) ) {
+                       $bits['index'] = '';
+               }
+               return $bits;
+       }
+
+       /**
+        * Split an <ext> node into an associative array containing name, attr, inner and close
+        * All values in the resulting array are PPNodes. Inner and close are optional.
+        */
+       function splitExt() {
+               $bits = array();
+               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+                       if ( !isset( $child->name ) ) {
+                               continue;
+                       }
+                       if ( $child->name == 'name' ) {
+                               $bits['name'] = $child;
+                       } elseif ( $child->name == 'attr' ) {
+                               $bits['attr'] = $child;
+                       } elseif ( $child->name == 'inner' ) {
+                               $bits['inner'] = $child;
+                       } elseif ( $child->name == 'close' ) {
+                               $bits['close'] = $child;
+                       }
+               }
+               if ( !isset( $bits['name'] ) ) {
+                       throw new MWException( 'Invalid ext node passed to ' . __METHOD__ );
+               }
+               return $bits;
+       }
+
+       /**
+        * Split an <h> node
+        */
+       function splitHeading() {
+               if ( $this->name !== 'h' ) {
+                       throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+               }
+               $bits = array();
+               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+                       if ( !isset( $child->name ) ) {
+                               continue;
+                       }
+                       if ( $child->name == 'i' ) {
+                               $bits['i'] = $child->value;
+                       } elseif ( $child->name == 'level' ) {
+                               $bits['level'] = $child->value;
+                       }
+               }
+               if ( !isset( $bits['i'] ) ) {
+                       throw new MWException( 'Invalid h node passed to ' . __METHOD__ );
+               }
+               return $bits;
+       }
+
+       /**
+        * Split a <template> or <tplarg> node
+        */
+       function splitTemplate() {
+               $parts = array();
+               $bits = array( 'lineStart' => '' );
+               for ( $child = $this->firstChild; $child; $child = $child->nextSibling ) {
+                       if ( !isset( $child->name ) ) {
+                               continue;
+                       }
+                       if ( $child->name == 'title' ) {
+                               $bits['title'] = $child;
+                       }
+                       if ( $child->name == 'part' ) {
+                               $parts[] = $child;
+                       }
+                       if ( $child->name == 'lineStart' ) {
+                               $bits['lineStart'] = '1';
+                       }
+               }
+               if ( !isset( $bits['title'] ) ) {
+                       throw new MWException( 'Invalid node passed to ' . __METHOD__ );
+               }
+               $bits['parts'] = new PPNode_Hash_Array( $parts );
+               return $bits;
+       }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Text implements PPNode {
+       var $value, $nextSibling;
+
+       function __construct( $value ) {
+               if ( is_object( $value ) ) {
+                       throw new MWException( __CLASS__ . ' given object instead of string' );
+               }
+               $this->value = $value;
+       }
+
+       function __toString() {
+               return htmlspecialchars( $this->value );
+       }
+
+       function getNextSibling() {
+               return $this->nextSibling;
+       }
+
+       function getChildren() { return false; }
+       function getFirstChild() { return false; }
+       function getChildrenOfType( $name ) { return false; }
+       function getLength() { return false; }
+       function item( $i ) { return false; }
+       function getName() { return '#text'; }
+       function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+       function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+       function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Array implements PPNode {
+       var $value, $nextSibling;
+
+       function __construct( $value ) {
+               $this->value = $value;
+       }
+
+       function __toString() {
+               return var_export( $this, true );
+       }
+
+       function getLength() {
+               return count( $this->value );
+       }
+
+       function item( $i ) {
+               return $this->value[$i];
+       }
+
+       function getName() { return '#nodelist'; }
+
+       function getNextSibling() {
+               return $this->nextSibling;
+       }
+
+       function getChildren() { return false; }
+       function getFirstChild() { return false; }
+       function getChildrenOfType( $name ) { return false; }
+       function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+       function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+       function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
+
+/**
+ * @ingroup Parser
+ */
+class PPNode_Hash_Attr implements PPNode {
+       var $name, $value, $nextSibling;
+
+       function __construct( $name, $value ) {
+               $this->name = $name;
+               $this->value = $value;
+       }
+
+       function __toString() {
+               return "<@{$this->name}>" . htmlspecialchars( $this->value ) . "</@{$this->name}>";
+       }
+
+       function getName() {
+               return $this->name;
+       }
+
+       function getNextSibling() {
+               return $this->nextSibling;
+       }
+
+       function getChildren() { return false; }
+       function getFirstChild() { return false; }
+       function getChildrenOfType( $name ) { return false; }
+       function getLength() { return false; }
+       function item( $i ) { return false; }
+       function splitArg() { throw new MWException( __METHOD__ . ': not supported' ); }
+       function splitExt() { throw new MWException( __METHOD__ . ': not supported' ); }
+       function splitHeading() { throw new MWException( __METHOD__ . ': not supported' ); }
+}
diff --git a/includes/specials/Allmessages.php b/includes/specials/Allmessages.php
new file mode 100644 (file)
index 0000000..c2a8de4
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+/**
+ * Use this special page to get a list of the MediaWiki system messages.
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor.
+ */
+function wfSpecialAllmessages() {
+       global $wgOut, $wgRequest, $wgMessageCache, $wgTitle;
+       global $wgUseDatabaseMessages;
+
+       # The page isn't much use if the MediaWiki namespace is not being used
+       if( !$wgUseDatabaseMessages ) {
+               $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
+               return;
+       }
+
+       wfProfileIn( __METHOD__ );
+
+       wfProfileIn( __METHOD__ . '-setup' );
+       $ot = $wgRequest->getText( 'ot' );
+
+       $navText = wfMsg( 'allmessagestext' );
+
+       # Make sure all extension messages are available
+
+       $wgMessageCache->loadAllMessages();
+
+       $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) );
+       ksort( $sortedArray );
+       $messages = array();
+
+       foreach ( $sortedArray as $key => $value ) {
+               $messages[$key]['enmsg'] = $value;
+               $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false ); // wfMsgNoDbNoTrans doesn't exist
+               $messages[$key]['msg'] = wfMsgNoTrans( $key );
+       }
+
+       wfProfileOut( __METHOD__ . '-setup' );
+
+       wfProfileIn( __METHOD__ . '-output' );
+       $wgOut->addScriptFile( 'allmessages.js' );
+       if ( $ot == 'php' ) {
+               $navText .= wfAllMessagesMakePhp( $messages );
+               $wgOut->addHTML( 'PHP | <a href="' . $wgTitle->escapeLocalUrl( 'ot=html' ) . '">HTML</a> | ' .
+                       '<a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' .
+                       '<pre>' . htmlspecialchars( $navText ) . '</pre>' );
+       } else if ( $ot == 'xml' ) {
+               $wgOut->disable();
+               header( 'Content-type: text/xml' );
+               echo wfAllMessagesMakeXml( $messages );
+       } else {
+               $wgOut->addHTML( '<a href="' . $wgTitle->escapeLocalUrl( 'ot=php' ) . '">PHP</a> | ' .
+                       'HTML |  <a href="' . $wgTitle->escapeLocalUrl( 'ot=xml' ) . '">XML</a>' );
+               $wgOut->addWikiText( $navText );
+               $wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) );
+       }
+       wfProfileOut( __METHOD__ . '-output' );
+
+       wfProfileOut( __METHOD__ );
+}
+
+function wfAllMessagesMakeXml( $messages ) {
+       global $wgLang;
+       $lang = $wgLang->getCode();
+       $txt = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
+       $txt .= "<messages lang=\"$lang\">\n";
+       foreach( $messages as $key => $m ) {
+               $txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n";
+       }
+       $txt .= "</messages>";
+       return $txt;
+}
+
+/**
+ * Create the messages array, formatted in PHP to copy to language files.
+ * @param $messages Messages array.
+ * @return The PHP messages array.
+ * @todo Make suitable for language files.
+ */
+function wfAllMessagesMakePhp( $messages ) {
+       global $wgLang;
+       $txt = "\n\n\$messages = array(\n";
+       foreach( $messages as $key => $m ) {
+               if( $wgLang->getCode() != 'en' && $m['msg'] == $m['enmsg'] ) {
+                       continue;
+               } else if ( wfEmptyMsg( $key, $m['msg'] ) ) {
+                       $m['msg'] = '';
+                       $comment = ' #empty';
+               } else {
+                       $comment = '';
+               }
+               $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
+       }
+       $txt .= ');';
+       return $txt;
+}
+
+/**
+ * Create a list of messages, formatted in HTML as a list of messages and values and showing differences between the default language file message and the message in MediaWiki: namespace.
+ * @param $messages Messages array.
+ * @return The HTML list of messages.
+ */
+function wfAllMessagesMakeHTMLText( $messages ) {
+       global $wgLang, $wgContLang, $wgUser;
+       wfProfileIn( __METHOD__ );
+
+       $sk = $wgUser->getSkin();
+       $talk = wfMsg( 'talkpagelinktext' );
+
+       $input = Xml::element( 'input', array(
+               'type'    => 'text',
+               'id'      => 'allmessagesinput',
+               'onkeyup' => 'allmessagesfilter()'
+       ), '' );
+       $checkbox = Xml::element( 'input', array(
+               'type'    => 'button',
+               'value'   => wfMsgHtml( 'allmessagesmodified' ),
+               'id'      => 'allmessagescheckbox',
+               'onclick' => 'allmessagesmodified()'
+       ), '' );
+
+       $txt = '<span id="allmessagesfilter" style="display: none;">' . wfMsgHtml( 'allmessagesfilter' ) . " {$input}{$checkbox} " . '</span>';
+
+       $txt .= '
+<table border="1" cellspacing="0" width="100%" id="allmessagestable">
+       <tr>
+               <th rowspan="2">' . wfMsgHtml( 'allmessagesname' ) . '</th>
+               <th>' . wfMsgHtml( 'allmessagesdefault' ) . '</th>
+       </tr>
+       <tr>
+               <th>' . wfMsgHtml( 'allmessagescurrent' ) . '</th>
+       </tr>';
+
+       wfProfileIn( __METHOD__ . "-check" );
+
+       # This is a nasty hack to avoid doing independent existence checks
+       # without sending the links and table through the slow wiki parser.
+       $pageExists = array(
+               NS_MEDIAWIKI => array(),
+               NS_MEDIAWIKI_TALK => array()
+       );
+       $dbr = wfGetDB( DB_SLAVE );
+       $page = $dbr->tableName( 'page' );
+       $sql = "SELECT page_namespace,page_title FROM $page WHERE page_namespace IN (" . NS_MEDIAWIKI . ", " . NS_MEDIAWIKI_TALK . ")";
+       $res = $dbr->query( $sql );
+       while( $s = $dbr->fetchObject( $res ) ) {
+               $pageExists[$s->page_namespace][$s->page_title] = true;
+       }
+       $dbr->freeResult( $res );
+       wfProfileOut( __METHOD__ . "-check" );
+
+       wfProfileIn( __METHOD__ . "-output" );
+
+       $i = 0;
+
+       foreach( $messages as $key => $m ) {
+               $title = $wgLang->ucfirst( $key );
+               if( $wgLang->getCode() != $wgContLang->getCode() ) {
+                       $title .= '/' . $wgLang->getCode();
+               }
+
+               $titleObj =& Title::makeTitle( NS_MEDIAWIKI, $title );
+               $talkPage =& Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
+
+               $changed = ( $m['statmsg'] != $m['msg'] );
+               $message = htmlspecialchars( $m['statmsg'] );
+               $mw = htmlspecialchars( $m['msg'] );
+
+               if( isset( $pageExists[NS_MEDIAWIKI][$title] ) ) {
+                       $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .  htmlspecialchars( $key ) . '</span>' );
+               } else {
+                       $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .  htmlspecialchars( $key ) . '</span>' );
+               }
+               if( isset( $pageExists[NS_MEDIAWIKI_TALK][$title] ) ) {
+                       $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
+               } else {
+                       $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
+               }
+
+               $anchor = 'msg_' . htmlspecialchars( strtolower( $title ) );
+               $anchor = "<a id=\"$anchor\" name=\"$anchor\"></a>";
+
+               if( $changed ) {
+                       $txt .= "
+       <tr class=\"orig\" id=\"sp-allmessages-r1-$i\">
+               <td rowspan=\"2\">
+                       $anchor$pageLink<br />$talkLink
+               </td><td>
+$message
+               </td>
+       </tr><tr class=\"new\" id=\"sp-allmessages-r2-$i\">
+               <td>
+$mw
+               </td>
+       </tr>";
+               } else {
+                       $txt .= "
+       <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
+               <td>
+                       $anchor$pageLink<br />$talkLink
+               </td><td>
+$mw
+               </td>
+       </tr>";
+               }
+               $i++;
+       }
+       $txt .= '</table>';
+       wfProfileOut( __METHOD__ . '-output' );
+
+       wfProfileOut( __METHOD__ );
+       return $txt;
+}
diff --git a/includes/specials/Allpages.php b/includes/specials/Allpages.php
new file mode 100644 (file)
index 0000000..7223e31
--- /dev/null
@@ -0,0 +1,404 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Allpages/FOO (default NULL)
+ * @param $specialPage See the SpecialPage object.
+ */
+function wfSpecialAllpages( $par=NULL, $specialPage ) {
+       global $wgRequest, $wgOut, $wgContLang;
+
+       # GET values
+       $from = $wgRequest->getVal( 'from' );
+       $namespace = $wgRequest->getInt( 'namespace' );
+
+       $namespaces = $wgContLang->getNamespaces();
+
+       $indexPage = new SpecialAllpages();
+
+       $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) )  ?
+               wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
+               wfMsg( 'allarticles' )
+               );
+
+       if ( isset($par) ) {
+               $indexPage->showChunk( $namespace, $par, $specialPage->including() );
+       } elseif ( isset($from) ) {
+               $indexPage->showChunk( $namespace, $from, $specialPage->including() );
+       } else {
+               $indexPage->showToplevel ( $namespace, $specialPage->including() );
+       }
+}
+
+/**
+ * Implements Special:Allpages
+ * @ingroup SpecialPage
+ */
+class SpecialAllpages {
+       /**
+        * Maximum number of pages to show on single subpage.
+        */
+       protected $maxPerPage = 960;
+
+       /**
+        * Name of this special page. Used to make title objects that reference back
+        * to this page.
+        */
+       protected $name = 'Allpages';
+
+       /**
+        * Determines, which message describes the input field 'nsfrom'.
+        */
+       protected $nsfromMsg = 'allpagesfrom';
+
+/**
+ * HTML for the top form
+ * @param integer $namespace A namespace constant (default NS_MAIN).
+ * @param string $from Article name we are starting listing at.
+ */
+function namespaceForm ( $namespace = NS_MAIN, $from = '' ) {
+       global $wgScript;
+       $t = SpecialPage::getTitleFor( $this->name );
+
+       $out  = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
+       $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+       $out .= Xml::hidden( 'title', $t->getPrefixedText() );
+       $out .= Xml::openElement( 'fieldset' );
+       $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
+       $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
+       $out .= "<tr>
+                       <td class='mw-label'>" .
+                               Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) .
+                       "</td>
+                       <td class='mw-input'>" .
+                               Xml::input( 'from', 20, $from, array( 'id' => 'nsfrom' ) ) .
+                       "</td>
+               </tr>
+               <tr>
+                       <td class='mw-label'>" .
+                               Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+                       "</td>
+                       <td class='mw-input'>" .
+                               Xml::namespaceSelector( $namespace, null ) . ' ' .
+                               Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+                       "</td>
+                       </tr>";
+       $out .= Xml::closeElement( 'table' );
+       $out .= Xml::closeElement( 'fieldset' );
+       $out .= Xml::closeElement( 'form' );
+       $out .= Xml::closeElement( 'div' );
+       return $out;
+}
+
+/**
+ * @param integer $namespace (default NS_MAIN)
+ */
+function showToplevel ( $namespace = NS_MAIN, $including = false ) {
+       global $wgOut, $wgContLang;
+       $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+       # TODO: Either make this *much* faster or cache the title index points
+       # in the querycache table.
+
+       $dbr = wfGetDB( DB_SLAVE );
+       $out = "";
+       $where = array( 'page_namespace' => $namespace );
+
+       global $wgMemc;
+       $key = wfMemcKey( 'allpages', 'ns', $namespace );
+       $lines = $wgMemc->get( $key );
+
+       if( !is_array( $lines ) ) {
+               $options = array( 'LIMIT' => 1 );
+               if ( ! $dbr->implicitOrderby() ) {
+                       $options['ORDER BY'] = 'page_title';
+               }
+               $firstTitle = $dbr->selectField( 'page', 'page_title', $where, __METHOD__, $options );
+               $lastTitle = $firstTitle;
+
+               # This array is going to hold the page_titles in order.
+               $lines = array( $firstTitle );
+
+               # If we are going to show n rows, we need n+1 queries to find the relevant titles.
+               $done = false;
+               for( $i = 0; !$done; ++$i ) {
+                       // Fetch the last title of this chunk and the first of the next
+                       $chunk = is_null( $lastTitle )
+                               ? ''
+                               : 'page_title >= ' . $dbr->addQuotes( $lastTitle );
+                       $res = $dbr->select(
+                               'page', /* FROM */
+                               'page_title', /* WHAT */
+                               $where + array($chunk),
+                               __METHOD__,
+                               array ('LIMIT' => 2, 'OFFSET' => $this->maxPerPage - 1, 'ORDER BY' => 'page_title') );
+
+                       if ( $s = $dbr->fetchObject( $res ) ) {
+                               array_push( $lines, $s->page_title );
+                       } else {
+                               // Final chunk, but ended prematurely. Go back and find the end.
+                               $endTitle = $dbr->selectField( 'page', 'MAX(page_title)',
+                                       array(
+                                               'page_namespace' => $namespace,
+                                               $chunk
+                                       ), __METHOD__ );
+                               array_push( $lines, $endTitle );
+                               $done = true;
+                       }
+                       if( $s = $dbr->fetchObject( $res ) ) {
+                               array_push( $lines, $s->page_title );
+                               $lastTitle = $s->page_title;
+                       } else {
+                               // This was a final chunk and ended exactly at the limit.
+                               // Rare but convenient!
+                               $done = true;
+                       }
+                       $dbr->freeResult( $res );
+               }
+               $wgMemc->add( $key, $lines, 3600 );
+       }
+
+       // If there are only two or less sections, don't even display them.
+       // Instead, display the first section directly.
+       if( count( $lines ) <= 2 ) {
+               $this->showChunk( $namespace, '', $including );
+               return;
+       }
+
+       # At this point, $lines should contain an even number of elements.
+       $out .= "<table class='allpageslist' style='background: inherit;'>";
+       while ( count ( $lines ) > 0 ) {
+               $inpoint = array_shift ( $lines );
+               $outpoint = array_shift ( $lines );
+               $out .= $this->showline ( $inpoint, $outpoint, $namespace, false );
+       }
+       $out .= '</table>';
+       $nsForm = $this->namespaceForm( $namespace, '', false );
+
+       # Is there more?
+       if ( $including ) {
+               $out2 = '';
+       } else {
+               $morelinks = '';
+               if ( $morelinks != '' ) {
+                       $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+                       $out2 .= '<tr valign="top"><td>' . $nsForm;
+                       $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">';
+                       $out2 .= $morelinks . '</td></tr></table><hr />';
+               } else {
+                       $out2 = $nsForm . '<hr />';
+               }
+       }
+
+       $wgOut->addHtml( $out2 . $out );
+}
+
+/**
+ * @todo Document
+ * @param string $from
+ * @param integer $namespace (Default NS_MAIN)
+ */
+function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
+       global $wgContLang;
+       $align = $wgContLang->isRtl() ? 'left' : 'right';
+       $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
+       $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
+       $queryparams = ($namespace ? "namespace=$namespace" : '');
+       $special = SpecialPage::getTitleFor( $this->name, $inpoint );
+       $link = $special->escapeLocalUrl( $queryparams );
+
+       $out = wfMsgHtml(
+               'alphaindexline',
+               "<a href=\"$link\">$inpointf</a></td><td><a href=\"$link\">",
+               "</a></td><td><a href=\"$link\">$outpointf</a>"
+       );
+       return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
+}
+
+/**
+ * @param integer $namespace (Default NS_MAIN)
+ * @param string $from list all pages from this name (default FALSE)
+ */
+function showChunk( $namespace = NS_MAIN, $from, $including = false ) {
+       global $wgOut, $wgUser, $wgContLang;
+
+       $sk = $wgUser->getSkin();
+
+       $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+       $namespaces = $wgContLang->getNamespaces();
+       $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+       $n = 0;
+
+       if ( !$fromList ) {
+               $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+       } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+               // Show errormessage and reset to NS_MAIN
+               $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+               $namespace = NS_MAIN;
+       } else {
+               list( $namespace, $fromKey, $from ) = $fromList;
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'page',
+                       array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+                       array(
+                               'page_namespace' => $namespace,
+                               'page_title >= ' . $dbr->addQuotes( $fromKey )
+                       ),
+                       __METHOD__,
+                       array(
+                               'ORDER BY'  => 'page_title',
+                               'LIMIT'     => $this->maxPerPage + 1,
+                               'USE INDEX' => 'name_title',
+                       )
+               );
+
+               if( $res->numRows() > 0 ) {
+                       $out = '<table style="background: inherit;" border="0" width="100%">';
+       
+                       while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+                               $t = Title::makeTitle( $s->page_namespace, $s->page_title );
+                               if( $t ) {
+                                       $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
+                                               $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
+                                               ($s->page_is_redirect ? '</div>' : '' );
+                               } else {
+                                       $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+                               }
+                               if( $n % 3 == 0 ) {
+                                       $out .= '<tr>';
+                               }
+                               $out .= "<td width=\"33%\">$link</td>";
+                               $n++;
+                               if( $n % 3 == 0 ) {
+                                       $out .= '</tr>';
+                               }
+                       }
+                       if( ($n % 3) != 0 ) {
+                               $out .= '</tr>';
+                       }
+                       $out .= '</table>';
+               } else {
+                       $out = '';
+               }
+       }
+
+       if ( $including ) {
+               $out2 = '';
+       } else {
+               if( $from == '' ) {
+                       // First chunk; no previous link.
+                       $prevTitle = null;
+               } else {
+                       # Get the last title from previous chunk
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res_prev = $dbr->select(
+                               'page',
+                               'page_title',
+                               array( 'page_namespace' => $namespace, 'page_title < '.$dbr->addQuotes($from) ),
+                               __METHOD__,
+                               array( 'ORDER BY' => 'page_title DESC', 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 ) )
+                       );
+
+                       # Get first title of previous complete chunk
+                       if( $dbr->numrows( $res_prev ) >= $this->maxPerPage ) {
+                               $pt = $dbr->fetchObject( $res_prev );
+                               $prevTitle = Title::makeTitle( $namespace, $pt->page_title );
+                       } else {
+                               # The previous chunk is not complete, need to link to the very first title
+                               # available in the database
+                               $options = array( 'LIMIT' => 1 );
+                               if ( ! $dbr->implicitOrderby() ) {
+                                       $options['ORDER BY'] = 'page_title';
+                               }
+                               $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), __METHOD__, $options );
+                               # Show the previous link if it s not the current requested chunk
+                               if( $from != $reallyFirstPage_title ) {
+                                       $prevTitle =  Title::makeTitle( $namespace, $reallyFirstPage_title );
+                               } else {
+                                       $prevTitle = null;
+                               }
+                       }
+               }
+
+               $nsForm = $this->namespaceForm( $namespace, $from );
+               $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+               $out2 .= '<tr valign="top"><td>' . $nsForm;
+               $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
+                               $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
+                                       wfMsgHtml ( 'allpages' ) );
+
+               $self = SpecialPage::getTitleFor( 'Allpages' );
+
+               # Do we put a previous link ?
+               if( isset( $prevTitle ) &&  $pt = $prevTitle->getText() ) {
+                       $q = 'from=' . $prevTitle->getPartialUrl()
+                               . ( $namespace ? '&namespace=' . $namespace : '' );
+                       $prevLink = $sk->makeKnownLinkObj( $self,
+                               wfMsgHTML( 'prevpage', htmlspecialchars( $pt ) ), $q );
+                       $out2 .= ' | ' . $prevLink;
+               }
+
+               if( $n == $this->maxPerPage && $s = $dbr->fetchObject($res) ) {
+                       # $s is the first link of the next chunk
+                       $t = Title::MakeTitle($namespace, $s->page_title);
+                       $q = 'from=' . $t->getPartialUrl()
+                               . ( $namespace ? '&namespace=' . $namespace : '' );
+                       $nextLink = $sk->makeKnownLinkObj( $self,
+                               wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q );
+                       $out2 .= ' | ' . $nextLink;
+               }
+               $out2 .= "</td></tr></table><hr />";
+       }
+
+       $wgOut->addHtml( $out2 . $out );
+       if( isset($prevLink) or isset($nextLink) ) {
+               $wgOut->addHtml( '<hr /><p style="font-size: smaller; float: ' . $align . '">' );
+               if( isset( $prevLink ) ) {
+                       $wgOut->addHTML( $prevLink );
+               }
+               if( isset( $prevLink ) && isset( $nextLink ) ) {
+                       $wgOut->addHTML( ' | ' );
+               }
+               if( isset( $nextLink ) ) {
+                       $wgOut->addHTML( $nextLink );
+               }
+               $wgOut->addHTML( '</p>' );
+
+       }
+
+}
+
+/**
+ * @param int $ns the namespace of the article
+ * @param string $text the name of the article
+ * @return array( int namespace, string dbkey, string pagename ) or NULL on error
+ * @static (sort of)
+ * @access private
+ */
+function getNamespaceKeyAndText ($ns, $text) {
+       if ( $text == '' )
+               return array( $ns, '', '' ); # shortcut for common case
+
+       $t = Title::makeTitleSafe($ns, $text);
+       if ( $t && $t->isLocal() ) {
+               return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
+       } else if ( $t ) {
+               return NULL;
+       }
+
+       # try again, in case the problem was an empty pagename
+       $text = preg_replace('/(#|$)/', 'X$1', $text);
+       $t = Title::makeTitleSafe($ns, $text);
+       if ( $t && $t->isLocal() ) {
+               return array( $t->getNamespace(), '', '' );
+       } else {
+               return NULL;
+       }
+}
+}
diff --git a/includes/specials/Ancientpages.php b/includes/specials/Ancientpages.php
new file mode 100644 (file)
index 0000000..724d34b
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Implements Special:Ancientpages
+ * @ingroup SpecialPage
+ */
+class AncientPagesPage extends QueryPage {
+
+       function getName() {
+               return "Ancientpages";
+       }
+
+       function isExpensive() {
+               return true;
+       }
+
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               global $wgDBtype;
+               $db = wfGetDB( DB_SLAVE );
+               $page = $db->tableName( 'page' );
+               $revision = $db->tableName( 'revision' );
+               #$use_index = $db->useIndexClause( 'cur_timestamp' ); # FIXME! this is gone
+               $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' :
+                       'EXTRACT(epoch FROM rev_timestamp)';
+               return
+                       "SELECT 'Ancientpages' as type,
+                                       page_namespace as namespace,
+                               page_title as title,
+                               $epoch as value
+                       FROM $page, $revision
+                       WHERE page_namespace=".NS_MAIN." AND page_is_redirect=0
+                         AND page_latest=rev_id";
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+
+               $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $result->value ), true );
+               $title = Title::makeTitle( $result->namespace, $result->title );
+               $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+               return wfSpecialList($link, $d);
+       }
+}
+
+function wfSpecialAncientpages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $app = new AncientPagesPage();
+
+       $app->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Blockip.php b/includes/specials/Blockip.php
new file mode 100644 (file)
index 0000000..5ea25ca
--- /dev/null
@@ -0,0 +1,494 @@
+<?php
+/**
+ * Constructor for Special:Blockip page
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialBlockip( $par ) {
+       global $wgUser, $wgOut, $wgRequest;
+
+       # Can't block when the database is locked
+       if( wfReadOnly() ) {
+               $wgOut->readOnlyPage();
+               return;
+       }
+
+       # Permission check
+       if( !$wgUser->isAllowed( 'block' ) ) {
+               $wgOut->permissionRequired( 'block' );
+               return;
+       }
+
+       $ipb = new IPBlockForm( $par );
+
+       $action = $wgRequest->getVal( 'action' );
+       if ( 'success' == $action ) {
+               $ipb->showSuccess();
+       } else if ( $wgRequest->wasPosted() && 'submit' == $action &&
+               $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+               $ipb->doSubmit();
+       } else {
+               $ipb->showForm( '' );
+       }
+}
+
+/**
+ * Form object for the Special:Blockip page.
+ *
+ * @ingroup SpecialPage
+ */
+class IPBlockForm {
+       var $BlockAddress, $BlockExpiry, $BlockReason;
+#      var $BlockEmail;
+
+       function IPBlockForm( $par ) {
+               global $wgRequest, $wgUser;
+
+               $this->BlockAddress = $wgRequest->getVal( 'wpBlockAddress', $wgRequest->getVal( 'ip', $par ) );
+               $this->BlockAddress = strtr( $this->BlockAddress, '_', ' ' );
+               $this->BlockReason = $wgRequest->getText( 'wpBlockReason' );
+               $this->BlockReasonList = $wgRequest->getText( 'wpBlockReasonList' );
+               $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') );
+               $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' );
+
+               # Unchecked checkboxes are not included in the form data at all, so having one
+               # that is true by default is a bit tricky
+               $byDefault = !$wgRequest->wasPosted();
+               $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault );
+               $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault );
+               $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault );
+               $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false );
+               $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false );
+               # Re-check user's rights to hide names, very serious, defaults to 0
+               $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0;
+       }
+
+       function showForm( $err ) {
+               global $wgOut, $wgUser, $wgSysopUserBans;
+
+               $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+               $wgOut->addWikiMsg( 'blockiptext' );
+
+               if($wgSysopUserBans) {
+                       $mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' );
+               } else {
+                       $mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' );
+               }
+               $mIpbexpiry = Xml::label( wfMsg( 'ipbexpiry' ), 'wpBlockExpiry' );
+               $mIpbother = Xml::label( wfMsg( 'ipbother' ), 'mw-bi-other' );
+               $mIpbreasonother = Xml::label( wfMsg( 'ipbreason' ), 'wpBlockReasonList' );
+               $mIpbreason = Xml::label( wfMsg( 'ipbotherreason' ), 'mw-bi-reason' );
+
+               $titleObj = SpecialPage::getTitleFor( 'Blockip' );
+
+               if ( "" != $err ) {
+                       $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
+                       $wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $err ) );
+               }
+
+               $scBlockExpiryOptions = wfMsgForContent( 'ipboptions' );
+
+               $showblockoptions = $scBlockExpiryOptions != '-';
+               if (!$showblockoptions)
+                       $mIpbother = $mIpbexpiry;
+
+               $blockExpiryFormOptions = Xml::option( wfMsg( 'ipbotheroption' ), 'other' );
+               foreach (explode(',', $scBlockExpiryOptions) as $option) {
+                       if ( strpos($option, ":") === false ) $option = "$option:$option";
+                       list($show, $value) = explode(":", $option);
+                       $show = htmlspecialchars($show);
+                       $value = htmlspecialchars($value);
+                       $blockExpiryFormOptions .= Xml::option( $show, $value, $this->BlockExpiry === $value ? true : false ) . "\n";
+               }
+
+               $reasonDropDown = Xml::listDropDown( 'wpBlockReasonList',
+                       wfMsgForContent( 'ipbreason-dropdown' ),
+                       wfMsgForContent( 'ipbreasonotherlist' ), '', 'wpBlockDropDown', 4 );
+
+               global $wgStylePath, $wgStyleVersion;
+               $wgOut->addHTML(
+                       Xml::tags( 'script', array( 'type' => 'text/javascript', 'src' => "$wgStylePath/common/block.js?$wgStyleVersion" ), '' ) .
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( "action=submit" ), 'id' => 'blockip' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'blockip-legend' ) ) .
+                       Xml::openElement( 'table', array ( 'border' => '0', 'id' => 'mw-blockip-table' ) ) .
+                       "<tr>
+                               <td class='mw-label'>
+                                       {$mIpaddress}
+                               </td>
+                               <td class='mw-input'>" .
+                                       Xml::input( 'wpBlockAddress', 45, $this->BlockAddress,
+                                               array(
+                                                       'tabindex' => '1',
+                                                       'id' => 'mw-bi-target',
+                                                       'onchange' => 'updateBlockOptions()' ) ). "
+                               </td>
+                       </tr>
+                       <tr>"
+               );
+               if ( $showblockoptions ) {
+                       $wgOut->addHTML("
+                               <td class='mw-label'>
+                                       {$mIpbexpiry}
+                               </td>
+                               <td class='mw-input'>" .
+                                       Xml::tags( 'select',
+                                               array(
+                                                       'id' => 'wpBlockExpiry',
+                                                       'name' => 'wpBlockExpiry',
+                                                       'onchange' => 'considerChangingExpiryFocus()',
+                                                       'tabindex' => '2' ),
+                                               $blockExpiryFormOptions ) .
+                               "</td>"
+                       );
+               }
+               $wgOut->addHTML("
+                       </tr>
+                       <tr id='wpBlockOther'>
+                               <td class='mw-label'>
+                                       {$mIpbother}
+                               </td>
+                               <td class='mw-input'>" .
+                                       Xml::input( 'wpBlockOther', 45, $this->BlockOther,
+                                               array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class='mw-label'>
+                                       {$mIpbreasonother}
+                               </td>
+                               <td class='mw-input'>
+                                       {$reasonDropDown}
+                               </td>
+                       </tr>
+                       <tr id=\"wpBlockReason\">
+                               <td class='mw-label'>
+                                       {$mIpbreason}
+                               </td>
+                               <td class='mw-input'>" .
+                                       Xml::input( 'wpBlockReason', 45, $this->BlockReason,
+                                               array( 'tabindex' => '5', 'id' => 'mw-bi-reason', 'maxlength'=> '200' ) ) . "
+                               </td>
+                       </tr>
+                       <tr id='wpAnonOnlyRow'>
+                               <td>&nbsp;</td>
+                               <td class='mw-input'>" .
+                               Xml::checkLabel( wfMsg( 'ipbanononly' ),
+                                               'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly,
+                                               array( 'tabindex' => '6' ) ) . "
+                               </td>
+                       </tr>
+                       <tr id='wpCreateAccountRow'>
+                               <td>&nbsp;</td>
+                               <td class='mw-input'>" .
+                                       Xml::checkLabel( wfMsg( 'ipbcreateaccount' ),
+                                               'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount,
+                                               array( 'tabindex' => '7' ) ) . "
+                               </td>
+                       </tr>
+                       <tr id='wpEnableAutoblockRow'>
+                               <td>&nbsp;</td>
+                               <td class='mw-input'>" .
+                                       Xml::checkLabel( wfMsg( 'ipbenableautoblock' ),
+                                               'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock,
+                                               array( 'tabindex' => '8' ) ) . "
+                               </td>
+                       </tr>"
+               );
+
+               global $wgSysopEmailBans;
+               if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+                       $wgOut->addHTML("
+                               <tr id='wpEnableEmailBan'>
+                                       <td>&nbsp;</td>
+                                       <td class='mw-input'>" .
+                                               Xml::checkLabel( wfMsg( 'ipbemailban' ),
+                                                       'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
+                                                       array( 'tabindex' => '9' )) . "
+                                       </td>
+                               </tr>"
+                       );
+               }
+
+               // Allow some users to hide name from block log, blocklist and listusers
+               if ( $wgUser->isAllowed( 'hideuser' ) ) {
+                       $wgOut->addHTML("
+                               <tr id='wpEnableHideUser'>
+                                       <td>&nbsp;</td>
+                                       <td class='mw-input'>" .
+                                               Xml::checkLabel( wfMsg( 'ipbhidename' ),
+                                                       'wpHideName', 'wpHideName', $this->BlockHideName,
+                                                       array( 'tabindex' => '10' ) ) . "
+                                       </td>
+                               </tr>"
+                       );
+               }
+               
+               # Watchlist their user page?
+               $wgOut->addHTML("
+                       <tr id='wpEnableWatchUser'>
+                               <td>&nbsp;</td>
+                               <td class='mw-input'>" .
+                                       Xml::checkLabel( wfMsg( 'ipbwatchuser' ),
+                                               'wpWatchUser', 'wpWatchUser', $this->BlockWatchUser,
+                                               array( 'tabindex' => '11' ) ) . "
+                               </td>
+                       </tr>"
+               );
+
+               $wgOut->addHTML("
+                       <tr>
+                               <td style='padding-top: 1em'>&nbsp;</td>
+                               <td  class='mw-submit' style='padding-top: 1em'>" .
+                                       Xml::submitButton( wfMsg( 'ipbsubmit' ),
+                                               array( 'name' => 'wpBlock', 'tabindex' => '12' ) ) . "
+                               </td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) .
+                       Xml::tags( 'script', array( 'type' => 'text/javascript' ), 'updateBlockOptions()' ) . "\n"
+               );
+
+               $wgOut->addHtml( $this->getConvenienceLinks() );
+
+               $user = User::newFromName( $this->BlockAddress );
+               if( is_object( $user ) ) {
+                       $this->showLogFragment( $wgOut, $user->getUserPage() );
+               } elseif( preg_match( '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $this->BlockAddress ) ) {
+                       $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
+               } elseif( preg_match( '/^\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}/', $this->BlockAddress ) ) {
+                       $this->showLogFragment( $wgOut, Title::makeTitle( NS_USER, $this->BlockAddress ) );
+               }
+       }
+
+       /**
+        * Backend block code.
+        * $userID and $expiry will be filled accordingly
+        * @return array(message key, arguments) on failure, empty array on success
+        */
+       function doBlock(&$userId = null, &$expiry = null)
+       {
+               global $wgUser, $wgSysopUserBans, $wgSysopRangeBans;
+
+               $userId = 0;
+               # Expand valid IPv6 addresses, usernames are left as is
+               $this->BlockAddress = IP::sanitizeIP( $this->BlockAddress );
+               # isIPv4() and IPv6() are used for final validation
+               $rxIP4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';
+               $rxIP6 = '\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}:\w{1,4}';
+               $rxIP = "($rxIP4|$rxIP6)";
+
+               # Check for invalid specifications
+               if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
+                       $matches = array();
+                       if ( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) {
+                               # IPv4
+                               if ( $wgSysopRangeBans ) {
+                                       if ( !IP::isIPv4( $this->BlockAddress ) || $matches[2] < 16 || $matches[2] > 32 ) {
+                                               return array('ip_range_invalid');
+                                       }
+                                       $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+                               } else {
+                                       # Range block illegal
+                                       return array('range_block_disabled');
+                               }
+                       } else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) {
+                               # IPv6
+                               if ( $wgSysopRangeBans ) {
+                                       if ( !IP::isIPv6( $this->BlockAddress ) || $matches[2] < 64 || $matches[2] > 128 ) {
+                                               return array('ip_range_invalid');
+                                       }
+                                       $this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
+                               } else {
+                                       # Range block illegal
+                                       return array('range_block_disabled');
+                               }
+                       } else {
+                               # Username block
+                               if ( $wgSysopUserBans ) {
+                                       $user = User::newFromName( $this->BlockAddress );
+                                       if( !is_null( $user ) && $user->getId() ) {
+                                               # Use canonical name
+                                               $userId = $user->getId();
+                                               $this->BlockAddress = $user->getName();
+                                       } else {
+                                               return array('nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) );
+                                       }
+                               } else {
+                                       return array('badipaddress');
+                               }
+                       }
+               }
+
+               $reasonstr = $this->BlockReasonList;
+               if ( $reasonstr != 'other' && $this->BlockReason != '') {
+                       // Entry from drop down menu + additional comment
+                       $reasonstr .= ': ' . $this->BlockReason;
+               } elseif ( $reasonstr == 'other' ) {
+                       $reasonstr = $this->BlockReason;
+               }
+
+               $expirestr = $this->BlockExpiry;
+               if( $expirestr == 'other' )
+                       $expirestr = $this->BlockOther;
+
+               if (strlen($expirestr) == 0) {
+                       return array('ipb_expiry_invalid');
+               }
+               
+               if ( false === ($expiry = Block::parseExpiryInput( $expirestr )) ) {
+                       // Bad expiry.
+                       return array('ipb_expiry_invalid');
+               }
+               
+               if( $this->BlockHideName && $expiry != 'infinity' ) {
+                       // Bad expiry.
+                       return array('ipb_expiry_temp');
+               }
+
+               # Create block
+               # Note: for a user block, ipb_address is only for display purposes
+               $block = new Block( $this->BlockAddress, $userId, $wgUser->getId(),
+                       $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly,
+                       $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName,
+                       $this->BlockEmail );
+
+               if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) {
+
+                       if ( !$block->insert() ) {
+                               return array('ipb_already_blocked', htmlspecialchars($this->BlockAddress));
+                       }
+
+                       wfRunHooks('BlockIpComplete', array($block, $wgUser));
+
+                       if ( $this->BlockWatchUser ) { 
+                               $wgUser->addWatch ( Title::makeTitle( NS_USER, $this->BlockAddress ) );
+                       }
+
+                       # Prepare log parameters
+                       $logParams = array();
+                       $logParams[] = $expirestr;
+                       $logParams[] = $this->blockLogFlags();
+
+                       # Make log entry, if the name is hidden, put it in the oversight log
+                       $log_type = ($this->BlockHideName) ? 'suppress' : 'block';
+                       $log = new LogPage( $log_type );
+                       $log->addEntry( 'block', Title::makeTitle( NS_USER, $this->BlockAddress ),
+                         $reasonstr, $logParams );
+
+                       # Report to the user
+                       return array();
+               }
+               else
+                       return array('hookaborted');
+       }
+
+       /**
+        * UI entry point for blocking
+        * Wraps around doBlock()
+        */
+       function doSubmit()
+       {
+               global $wgOut;
+               $retval = $this->doBlock();
+               if(empty($retval)) {
+                       $titleObj = SpecialPage::getTitleFor( 'Blockip' );
+                       $wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
+                               urlencode( $this->BlockAddress ) ) );
+                       return;
+               }
+               $key = array_shift($retval);
+               $this->showForm(wfMsgReal($key, $retval));
+       }
+
+       function showSuccess() {
+               global $wgOut;
+
+               $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+               $wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
+               $text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
+               $wgOut->addHtml( $text );
+       }
+
+       function showLogFragment( $out, $title ) {
+               $out->addHtml( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
+               LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText() );
+       }
+
+       /**
+        * Return a comma-delimited list of "flags" to be passed to the log
+        * reader for this block, to provide more information in the logs
+        *
+        * @return array
+        */
+       private function blockLogFlags() {
+               $flags = array();
+               if( $this->BlockAnonOnly && IP::isIPAddress( $this->BlockAddress ) )
+                                       // when blocking a user the option 'anononly' is not available/has no effect -> do not write this into log
+                       $flags[] = 'anononly';
+               if( $this->BlockCreateAccount )
+                       $flags[] = 'nocreate';
+               if( !$this->BlockEnableAutoblock )
+                       $flags[] = 'noautoblock';
+               if ( $this->BlockEmail )
+                       $flags[] = 'noemail';
+               return implode( ',', $flags );
+       }
+
+       /**
+        * Builds unblock and block list links
+        *
+        * @return string
+        */
+       private function getConvenienceLinks() {
+               global $wgUser;
+               $skin = $wgUser->getSkin();
+               $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
+               $links[] = $this->getUnblockLink( $skin );
+               $links[] = $this->getBlockListLink( $skin );
+               return '<p class="mw-ipb-conveniencelinks">' . implode( ' | ', $links ) . '</p>';
+       }
+
+       /**
+        * Build a convenient link to unblock the given username or IP
+        * address, if available; otherwise link to a blank unblock
+        * form
+        *
+        * @param $skin Skin to use
+        * @return string
+        */
+       private function getUnblockLink( $skin ) {
+               $list = SpecialPage::getTitleFor( 'Ipblocklist' );
+               if( $this->BlockAddress ) {
+                       $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
+                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ),
+                               'action=unblock&ip=' . urlencode( $this->BlockAddress ) );
+               } else {
+                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ),      'action=unblock' );
+               }
+       }
+
+       /**
+        * Build a convenience link to the block list
+        *
+        * @param $skin Skin to use
+        * @return string
+        */
+       private function getBlockListLink( $skin ) {
+               $list = SpecialPage::getTitleFor( 'Ipblocklist' );
+               if( $this->BlockAddress ) {
+                       $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
+                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ),
+                               'ip=' . urlencode( $this->BlockAddress ) );
+               } else {
+                       return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) );
+               }
+       }
+}
diff --git a/includes/specials/Blockme.php b/includes/specials/Blockme.php
new file mode 100644 (file)
index 0000000..f222e3c
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialBlockme() {
+       global $wgRequest, $wgBlockOpenProxies, $wgOut, $wgProxyKey;
+
+       $ip = wfGetIP();
+
+       if( !$wgBlockOpenProxies || $wgRequest->getText( 'ip' ) != md5( $ip . $wgProxyKey ) ) {
+               $wgOut->addWikiMsg( 'proxyblocker-disabled' );
+               return;
+       }
+
+       $blockerName = wfMsg( "proxyblocker" );
+       $reason = wfMsg( "proxyblockreason" );
+
+       $u = User::newFromName( $blockerName );
+       $id = $u->idForName();
+       if ( !$id ) {
+               $u = User::newFromName( $blockerName );
+               $u->addToDatabase();
+               $u->setPassword( bin2hex( mt_rand(0, 0x7fffffff ) ) );
+               $u->saveSettings();
+               $id = $u->getID();
+       }
+
+       $block = new Block( $ip, 0, $id, $reason, wfTimestampNow() );
+       $block->insert();
+
+       $wgOut->addWikiMsg( "proxyblocksuccess" );
+}
diff --git a/includes/specials/Booksources.php b/includes/specials/Booksources.php
new file mode 100644 (file)
index 0000000..0690c5c
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * Special page outputs information on sourcing a book with a particular ISBN
+ * The parser creates links to this page when dealing with ISBNs in wikitext
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @todo Validate ISBNs using the standard check-digit method
+ * @ingroup SpecialPages
+ */
+class SpecialBookSources extends SpecialPage {
+
+       /**
+        * ISBN passed to the page, if any
+        */
+       private $isbn = '';
+
+       /**
+        * Constructor
+        */
+       public function __construct() {
+               parent::__construct( 'Booksources' );
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param $isbn ISBN passed as a subpage parameter
+        */
+       public function execute( $isbn ) {
+               global $wgOut, $wgRequest;
+               $this->setHeaders();
+               $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) );
+               $wgOut->addWikiMsg( 'booksources-summary' );
+               $wgOut->addHtml( $this->makeForm() );
+               if( strlen( $this->isbn ) > 0 )
+                       $this->showList();
+       }
+
+       /**
+        * Trim ISBN and remove characters which aren't required
+        *
+        * @param $isbn Unclean ISBN
+        * @return string
+        */
+       private function cleanIsbn( $isbn ) {
+               return trim( preg_replace( '![^0-9X]!', '', $isbn ) );
+       }
+
+       /**
+        * Generate a form to allow users to enter an ISBN
+        *
+        * @return string
+        */
+       private function makeForm() {
+               global $wgScript;
+               $title = self::getTitleFor( 'Booksources' );
+               $form  = '<fieldset><legend>' . wfMsgHtml( 'booksources-search-legend' ) . '</legend>';
+               $form .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+               $form .= Xml::hidden( 'title', $title->getPrefixedText() );
+               $form .= '<p>' . Xml::inputLabel( wfMsg( 'booksources-isbn' ), 'isbn', 'isbn', 20, $this->isbn );
+               $form .= '&nbsp;' . Xml::submitButton( wfMsg( 'booksources-go' ) ) . '</p>';
+               $form .= Xml::closeElement( 'form' );
+               $form .= '</fieldset>';
+               return $form;
+       }
+
+       /**
+        * Determine where to get the list of book sources from,
+        * format and output them
+        *
+        * @return string
+        */
+       private function showList() {
+               global $wgOut, $wgContLang;
+
+               # Hook to allow extensions to insert additional HTML,
+               # e.g. for API-interacting plugins and so on
+               wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) );
+
+               # Check for a local page such as Project:Book_sources and use that if available
+               $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language
+               if( is_object( $title ) && $title->exists() ) {
+                       $rev = Revision::newFromTitle( $title );
+                       $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
+                       return true;
+               }
+
+               # Fall back to the defaults given in the language file
+               $wgOut->addWikiMsg( 'booksources-text' );
+               $wgOut->addHtml( '<ul>' );
+               $items = $wgContLang->getBookstoreList();
+               foreach( $items as $label => $url )
+                       $wgOut->addHtml( $this->makeListItem( $label, $url ) );
+               $wgOut->addHtml( '</ul>' );
+               return true;
+       }
+
+       /**
+        * Format a book source list item
+        *
+        * @param $label Book source label
+        * @param $url Book source URL
+        * @return string
+        */
+       private function makeListItem( $label, $url ) {
+               $url = str_replace( '$1', $this->isbn, $url );
+               return '<li><a href="' . htmlspecialchars( $url ) . '">' . htmlspecialchars( $label ) . '</a></li>';
+       }
+}
diff --git a/includes/specials/BrokenRedirects.php b/includes/specials/BrokenRedirects.php
new file mode 100644 (file)
index 0000000..0a16e6d
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page listing redirects to non existent page. Those should be
+ * fixed to point to an existing page.
+ * @ingroup SpecialPage
+ */
+class BrokenRedirectsPage extends PageQueryPage {
+       var $targets = array();
+
+       function getName() {
+               return 'BrokenRedirects';
+       }
+
+       function isExpensive( ) { return true; }
+       function isSyndicated() { return false; }
+
+       function getPageHeader( ) {
+               return wfMsgExt( 'brokenredirectstext', array( 'parse' ) );
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
+
+               $sql = "SELECT 'BrokenRedirects'  AS type,
+                               p1.page_namespace AS namespace,
+                               p1.page_title     AS title,
+                               rd_namespace,
+                               rd_title
+                          FROM $redirect AS rd
+                   JOIN $page p1 ON (rd.rd_from=p1.page_id)
+                     LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title )
+                                 WHERE rd_namespace >= 0
+                                   AND p2.page_namespace IS NULL";
+               return $sql;
+       }
+
+       function getOrder() {
+               return '';
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgUser, $wgContLang;
+
+               $fromObj = Title::makeTitle( $result->namespace, $result->title );
+               if ( isset( $result->rd_title ) ) {
+                       $toObj = Title::makeTitle( $result->rd_namespace, $result->rd_title );
+               } else {
+                       $blinks = $fromObj->getBrokenLinksFrom(); # TODO: check for redirect, not for links
+                       if ( $blinks ) {
+                               $toObj = $blinks[0];
+                       } else {
+                               $toObj = false;
+                       }
+               }
+
+               // $toObj may very easily be false if the $result list is cached
+               if ( !is_object( $toObj ) ) {
+                       return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>';
+               }
+
+               $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' );
+               $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' );
+               $to   = $skin->makeBrokenLinkObj( $toObj );
+               $arr = $wgContLang->getArrow();
+
+               $out = "{$from} {$edit}";
+
+               if( $wgUser->isAllowed( 'delete' ) ) {
+                       $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' );
+                       $out .= " {$delete}";
+               }
+
+               $out .= " {$arr} {$to}";
+               return $out;
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialBrokenRedirects() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $sbr = new BrokenRedirectsPage();
+
+       return $sbr->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Categories.php b/includes/specials/Categories.php
new file mode 100644 (file)
index 0000000..951c222
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialCategories( $par=null ) {
+       global $wgOut, $wgRequest;
+
+       if( $par == '' ) {
+               $from = $wgRequest->getText( 'from' );
+       } else {
+               $from = $par;
+       }
+       $cap = new CategoryPager( $from );
+       $wgOut->addHTML(
+               wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
+               $cap->getStartForm( $from ) .
+               $cap->getNavigationBar() .
+               '<ul>' . $cap->getBody() . '</ul>' .
+               $cap->getNavigationBar()
+       );
+}
+
+/**
+ * TODO: Allow sorting by count.  We need to have a unique index to do this
+ * properly.
+ *
+ * @ingroup SpecialPage Pager
+ */
+class CategoryPager extends AlphabeticPager {
+       function __construct( $from ) {
+               parent::__construct();
+               $from = str_replace( ' ', '_', $from );
+               if( $from !== '' ) {
+                       global $wgCapitalLinks, $wgContLang;
+                       if( $wgCapitalLinks ) {
+                               $from = $wgContLang->ucfirst( $from );
+                       }
+                       $this->mOffset = $from;
+               }
+       }
+       
+       function getQueryInfo() {
+               global $wgRequest;
+               return array(
+                       'tables' => array( 'category' ),
+                       'fields' => array( 'cat_title','cat_pages' ),
+                       'conds' => array( 'cat_pages > 0' ), 
+                       'options' => array( 'USE INDEX' => 'cat_title' ),
+               );
+       }
+
+       function getIndexField() {
+#              return array( 'abc' => 'cat_title', 'count' => 'cat_pages' );
+               return 'cat_title';
+       }
+
+       function getDefaultQuery() {
+               parent::getDefaultQuery();
+               unset( $this->mDefaultQuery['from'] );
+       }
+#      protected function getOrderTypeMessages() {
+#              return array( 'abc' => 'special-categories-sort-abc',
+#                      'count' => 'special-categories-sort-count' );
+#      }
+
+       protected function getDefaultDirections() {
+#              return array( 'abc' => false, 'count' => true );
+               return false;
+       }
+
+       /* Override getBody to apply LinksBatch on resultset before actually outputting anything. */
+       public function getBody() {
+               if (!$this->mQueryDone) {
+                       $this->doQuery();
+               }
+               $batch = new LinkBatch;
+
+               $this->mResult->rewind();
+
+               while ( $row = $this->mResult->fetchObject() ) {
+                       $batch->addObj( Title::makeTitleSafe( NS_CATEGORY, $row->cat_title ) );
+               }
+               $batch->execute();
+               $this->mResult->rewind();
+               return parent::getBody();
+       }
+
+       function formatRow($result) {
+               global $wgLang;
+               $title = Title::makeTitle( NS_CATEGORY, $result->cat_title );
+               $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
+               $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ),
+                               $wgLang->formatNum( $result->cat_pages ) );
+               return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
+       }
+       
+       public function getStartForm( $from ) {
+               global $wgScript;
+               $t = SpecialPage::getTitleFor( 'Categories' );
+       
+               return
+                       Xml::tags( 'form', array( 'method' => 'get', 'action' => $wgScript ),
+                               Xml::hidden( 'title', $t->getPrefixedText() ) .
+                               Xml::fieldset( wfMsg( 'categories' ),
+                                       Xml::inputLabel( wfMsg( 'categoriesfrom' ),
+                                               'from', 'from', 20, $from ) .
+                                       ' ' .
+                                       Xml::submitButton( wfMsg( 'allpagessubmit' ) ) ) );
+       }
+}
diff --git a/includes/specials/Confirmemail.php b/includes/specials/Confirmemail.php
new file mode 100644 (file)
index 0000000..9075fb9
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * Special page allows users to request email confirmation message, and handles
+ * processing of the confirmation code when the link in the email is followed
+ *
+ * @ingroup SpecialPage
+ * @author Brion Vibber
+ * @author Rob Church <robchur@gmail.com>
+ */
+class EmailConfirmation extends UnlistedSpecialPage {
+
+       /**
+        * Constructor
+        */
+       public function __construct() {
+               parent::__construct( 'Confirmemail' );
+       }
+
+       /**
+        * Main execution point
+        *
+        * @param $code Confirmation code passed to the page
+        */
+       function execute( $code ) {
+               global $wgUser, $wgOut;
+               $this->setHeaders();
+               if( empty( $code ) ) {
+                       if( $wgUser->isLoggedIn() ) {
+                               if( User::isValidEmailAddr( $wgUser->getEmail() ) ) {
+                                       $this->showRequestForm();
+                               } else {
+                                       $wgOut->addWikiMsg( 'confirmemail_noemail' );
+                               }
+                       } else {
+                               $title = SpecialPage::getTitleFor( 'Userlogin' );
+                               $self = SpecialPage::getTitleFor( 'Confirmemail' );
+                               $skin = $wgUser->getSkin();
+                               $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $self->getPrefixedUrl() );
+                               $wgOut->addHtml( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
+                       }
+               } else {
+                       $this->attemptConfirm( $code );
+               }
+       }
+
+       /**
+        * Show a nice form for the user to request a confirmation mail
+        */
+       function showRequestForm() {
+               global $wgOut, $wgUser, $wgLang, $wgRequest;
+               if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getText( 'token' ) ) ) {
+                       $ok = $wgUser->sendConfirmationMail();
+                       if ( WikiError::isError( $ok ) ) {
+                               $wgOut->addWikiMsg( 'confirmemail_sendfailed', $ok->toString() );
+                       } else {
+                               $wgOut->addWikiMsg( 'confirmemail_sent' );
+                       }
+               } else {
+                       if( $wgUser->isEmailConfirmed() ) {
+                               $time = $wgLang->timeAndDate( $wgUser->mEmailAuthenticated, true );
+                               $wgOut->addWikiMsg( 'emailauthenticated', $time );
+                       }
+                       if( $wgUser->isEmailConfirmationPending() ) {
+                               $wgOut->addWikiMsg( 'confirmemail_pending' );
+                       }
+                       $wgOut->addWikiMsg( 'confirmemail_text' );
+                       $self = SpecialPage::getTitleFor( 'Confirmemail' );
+                       $form  = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+                       $form .= wfHidden( 'token', $wgUser->editToken() );
+                       $form .= wfSubmitButton( wfMsgHtml( 'confirmemail_send' ) );
+                       $form .= wfCloseElement( 'form' );
+                       $wgOut->addHtml( $form );
+               }
+       }
+
+       /**
+        * Attempt to confirm the user's email address and show success or failure
+        * as needed; if successful, take the user to log in
+        *
+        * @param $code Confirmation code
+        */
+       function attemptConfirm( $code ) {
+               global $wgUser, $wgOut;
+               $user = User::newFromConfirmationCode( $code );
+               if( is_object( $user ) ) {
+                       $user->confirmEmail();
+                       $user->saveSettings();
+                       $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
+                       $wgOut->addWikiMsg( $message );
+                       if( !$wgUser->isLoggedIn() ) {
+                               $title = SpecialPage::getTitleFor( 'Userlogin' );
+                               $wgOut->returnToMain( true, $title );
+                       }
+               } else {
+                       $wgOut->addWikiMsg( 'confirmemail_invalid' );
+               }
+       }
+
+}
+
+/**
+ * Special page allows users to cancel an email confirmation using the e-mail
+ * confirmation code
+ *
+ * @ingroup SpecialPage
+ */
+class EmailInvalidation extends UnlistedSpecialPage {
+
+       public function __construct() {
+               parent::__construct( 'Invalidateemail' );
+       }
+
+       function execute( $code ) {
+               $this->setHeaders();
+               $this->attemptInvalidate( $code );
+       }
+
+       /**
+        * Attempt to invalidate the user's email address and show success or failure
+        * as needed; if successful, link to main page
+        *
+        * @param $code Confirmation code
+        */
+       function attemptInvalidate( $code ) {
+               global $wgUser, $wgOut;
+               $user = User::newFromConfirmationCode( $code );
+               if( is_object( $user ) ) {
+                       $user->invalidateEmail();
+                       $user->saveSettings();
+                       $wgOut->addWikiMsg( 'confirmemail_invalidated' );
+                       if( !$wgUser->isLoggedIn() ) {
+                               $wgOut->returnToMain();
+                       }
+               } else {
+                       $wgOut->addWikiMsg( 'confirmemail_invalid' );
+               }
+       }
+}
diff --git a/includes/specials/Contributions.php b/includes/specials/Contributions.php
new file mode 100644 (file)
index 0000000..c9cbc18
--- /dev/null
@@ -0,0 +1,465 @@
+<?php
+/**
+ * Special:Contributions, show user contributions in a paged list
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Pager for Special:Contributions
+ * @ingroup SpecialPage Pager
+ */
+class ContribsPager extends ReverseChronologicalPager {
+       public $mDefaultDirection = true;
+       var $messages, $target;
+       var $namespace = '', $year = '', $month = '', $mDb;
+
+       function __construct( $target, $namespace = false, $year = false, $month = false ) {
+               parent::__construct();
+               foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
+                       $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+               }
+               $this->target = $target;
+               $this->namespace = $namespace;
+
+               $year = intval($year);
+               $month = intval($month);
+
+               $this->year = $year > 0 ? $year : false;
+               $this->month = ($month > 0 && $month < 13) ? $month : false;
+               $this->getDateCond();
+
+               $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
+       }
+
+       function getDefaultQuery() {
+               $query = parent::getDefaultQuery();
+               $query['target'] = $this->target;
+               $query['month'] = $this->month;
+               $query['year'] = $this->year;
+               return $query;
+       }
+
+       function getQueryInfo() {
+               list( $index, $userCond ) = $this->getUserCond();
+               $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() );
+               return array(
+                       'tables' => array( 'page', 'revision' ),
+                       'fields' => array(
+                               'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'rev_id', 'rev_page',
+                               'rev_text_id', 'rev_timestamp', 'rev_comment', 'rev_minor_edit', 'rev_user',
+                               'rev_user_text', 'rev_parent_id', 'rev_deleted'
+                       ),
+                       'conds' => $conds,
+                       'options' => array( 'USE INDEX' => $index )
+               );
+       }
+
+       function getUserCond() {
+               $condition = array();
+
+               if ( $this->target == 'newbies' ) {
+                       $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
+                       $condition[] = 'rev_user >' . (int)($max - $max / 100);
+                       $index = 'user_timestamp';
+               } else {
+                       $condition['rev_user_text'] = $this->target;
+                       $index = 'usertext_timestamp';
+               }
+               return array( $index, $condition );
+       }
+
+       function getNamespaceCond() {
+               if ( $this->namespace !== '' ) {
+                       return array( 'page_namespace' => (int)$this->namespace );
+               } else {
+                       return array();
+               }
+       }
+
+       function getDateCond() {
+               // Given an optional year and month, we need to generate a timestamp
+               // to use as "WHERE rev_timestamp <= result"
+               // Examples: year = 2006 equals < 20070101 (+000000)
+               // year=2005, month=1    equals < 20050201
+               // year=2005, month=12   equals < 20060101
+
+               if (!$this->year && !$this->month)
+                       return;
+
+               if ( $this->year ) {
+                       $year = $this->year;
+               }
+               else {
+                       // If no year given, assume the current one
+                       $year = gmdate( 'Y' );
+                       // If this month hasn't happened yet this year, go back to last year's month
+                       if( $this->month > gmdate( 'n' ) ) {
+                               $year--;
+                       }
+               }
+
+               if ( $this->month ) {
+                       $month = $this->month + 1;
+                       // For December, we want January 1 of the next year
+                       if ($month > 12) {
+                               $month = 1;
+                               $year++;
+                       }
+               }
+               else {
+                       // No month implies we want up to the end of the year in question
+                       $month = 1;
+                       $year++;
+               }
+
+               if ($year > 2032)
+                       $year = 2032;
+               $ymd = (int)sprintf( "%04d%02d01", $year, $month );
+
+               // Y2K38 bug
+               if ($ymd > 20320101)
+                       $ymd = 20320101;
+
+               $this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
+       }
+
+       function getIndexField() {
+               return 'rev_timestamp';
+       }
+
+       function getStartBody() {
+               return "<ul>\n";
+       }
+
+       function getEndBody() {
+               return "</ul>\n";
+       }
+
+       /**
+        * Generates each row in the contributions list.
+        *
+        * Contributions which are marked "top" are currently on top of the history.
+        * For these contributions, a [rollback] link is shown for users with roll-
+        * back privileges. The rollback link restores the most recent version that
+        * was not written by the target user.
+        *
+        * @todo This would probably look a lot nicer in a table.
+        */
+       function formatRow( $row ) {
+               wfProfileIn( __METHOD__ );
+
+               global $wgLang, $wgUser, $wgContLang;
+
+               $sk = $this->getSkin();
+               $rev = new Revision( $row );
+
+               $page = Title::makeTitle( $row->page_namespace, $row->page_title );
+               $link = $sk->makeKnownLinkObj( $page );
+               $difftext = $topmarktext = '';
+               if( $row->rev_id == $row->page_latest ) {
+                       $topmarktext .= '<strong>' . $this->messages['uctop'] . '</strong>';
+                       if( !$row->page_is_new ) {
+                               $difftext .= '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=0' ) . ')';
+                       } else {
+                               $difftext .= $this->messages['newarticle'];
+                       }
+
+                       if( !$page->getUserPermissionsErrors( 'rollback', $wgUser )
+                       &&  !$page->getUserPermissionsErrors( 'edit', $wgUser ) ) {
+                               $topmarktext .= ' '.$sk->generateRollback( $rev );
+                       }
+
+               }
+               # Is there a visible previous revision?
+               if( $rev->userCan(Revision::DELETED_TEXT) ) {
+                       $difftext = '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
+               } else {
+                       $difftext = '(' . $this->messages['diff'] . ')';
+               }
+               $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
+
+               $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
+               $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
+
+               if( $this->target == 'newbies' ) {
+                       $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
+                       $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') ';
+               } else {
+                       $userlink = '';
+               }
+
+               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+                       $d = '<span class="history-deleted">' . $d . '</span>';
+               }
+
+               if( $rev->getParentId() === 0 ) {
+                       $nflag = '<span class="newpage">' . $this->messages['newpageletter'] . '</span>';
+               } else {
+                       $nflag = '';
+               }
+
+               if( $row->rev_minor_edit ) {
+                       $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
+               } else {
+                       $mflag = '';
+               }
+
+               $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink}{$comment} {$topmarktext}";
+               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+                       $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+               }
+               $ret = "<li>$ret</li>\n";
+               wfProfileOut( __METHOD__ );
+               return $ret;
+       }
+
+       /**
+        * Get the Database object in use
+        *
+        * @return Database
+        */
+       public function getDatabase() {
+               return $this->mDb;
+       }
+
+}
+
+/**
+ * Special page "user contributions".
+ * Shows a list of the contributions of a user.
+ *
+ * @return     none
+ * @param      $par    String: (optional) user name of the user for which to show the contributions
+ */
+function wfSpecialContributions( $par = null ) {
+       global $wgUser, $wgOut, $wgLang, $wgRequest;
+
+       $options = array();
+
+       if ( isset( $par ) && $par == 'newbies' ) {
+               $target = 'newbies';
+               $options['contribs'] = 'newbie';
+       } elseif ( isset( $par ) ) {
+               $target = $par;
+       } else {
+               $target = $wgRequest->getVal( 'target' );
+       }
+
+       // check for radiobox
+       if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
+               $target = 'newbies';
+               $options['contribs'] = 'newbie';
+       }
+
+       if ( !strlen( $target ) ) {
+               $wgOut->addHTML( contributionsForm( '' ) );
+               return;
+       }
+
+       $options['limit'] = $wgRequest->getInt( 'limit', 50 );
+       $options['target'] = $target;
+
+       $nt = Title::makeTitleSafe( NS_USER, $target );
+       if ( !$nt ) {
+               $wgOut->addHTML( contributionsForm( '' ) );
+               return;
+       }
+       $id = User::idFromName( $nt->getText() );
+
+       if ( $target != 'newbies' ) {
+               $target = $nt->getText();
+               $wgOut->setSubtitle( contributionsSub( $nt, $id ) );
+       } else {
+               $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
+       }
+
+       if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
+               $options['namespace'] = intval( $ns );
+       } else {
+               $options['namespace'] = '';
+       }
+       if ( $wgUser->isAllowed( 'markbotedit' ) && $wgRequest->getBool( 'bot' ) ) {
+               $options['bot'] = '1';
+       }
+
+       $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+       # Offset overrides year/month selection
+       if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) {
+               $options['month'] = intval( $month );
+       } else {
+               $options['month'] = '';
+       }
+       if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) {
+               $options['year'] = intval( $year );
+       } else if( $options['month'] ) {
+               $thisMonth = intval( gmdate( 'n' ) );
+               $thisYear = intval( gmdate( 'Y' ) );
+               if( intval( $options['month'] ) > $thisMonth ) {
+                       $thisYear--;
+               }
+               $options['year'] = $thisYear;
+       } else {
+               $options['year'] = '';
+       }
+
+       wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
+
+       if( $skip ) {
+               $options['year'] = '';
+               $options['month'] = '';
+       }
+
+       $wgOut->addHTML( contributionsForm( $options ) );
+
+       $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] );
+       if ( !$pager->getNumRows() ) {
+               $wgOut->addWikiMsg( 'nocontribs' );
+               return;
+       }
+
+       # Show a message about slave lag, if applicable
+       if( ( $lag = $pager->getDatabase()->getLag() ) > 0 )
+               $wgOut->showLagWarning( $lag );
+
+       $wgOut->addHTML(
+               '<p>' . $pager->getNavigationBar() . '</p>' .
+               $pager->getBody() .
+               '<p>' . $pager->getNavigationBar() . '</p>' );
+
+       # If there were contributions, and it was a valid user or IP, show
+       # the appropriate "footer" message - WHOIS tools, etc.
+       if( $target != 'newbies' ) {
+               $message = IP::isIPAddress( $target )
+                       ? 'sp-contributions-footer-anon'
+                       : 'sp-contributions-footer';
+
+
+               $text = wfMsgNoTrans( $message, $target );
+               if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
+                       $wgOut->addHtml( '<div class="mw-contributions-footer">' );
+                       $wgOut->addWikiText( $text );
+                       $wgOut->addHtml( '</div>' );
+               }
+       }
+}
+
+/**
+ * Generates the subheading with links
+ * @param Title $nt Title object for the target
+ * @param integer $id User ID for the target
+ * @return String: appropriately-escaped HTML to be output literally
+ */
+function contributionsSub( $nt, $id ) {
+       global $wgSysopUserBans, $wgLang, $wgUser;
+
+       $sk = $wgUser->getSkin();
+
+       if ( 0 == $id ) {
+               $user = $nt->getText();
+       } else {
+               $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+       }
+       $talk = $nt->getTalkPage();
+       if( $talk ) {
+               # Talk page link
+               $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
+               if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) {
+                       # Block link
+                       if( $wgUser->isAllowed( 'block' ) )
+                               $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
+                       # Block log link
+                       $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
+               }
+               # Other logs link
+               $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
+
+               wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
+
+               $links = implode( ' | ', $tools );
+       }
+
+       // Old message 'contribsub' had one parameter, but that doesn't work for
+       // languages that want to put the "for" bit right after $user but before
+       // $links.  If 'contribsub' is around, use it for reverse compatibility,
+       // otherwise use 'contribsub2'.
+       if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
+               return wfMsgHtml( 'contribsub2', $user, $links );
+       } else {
+               return wfMsgHtml( 'contribsub', "$user ($links)" );
+       }
+}
+
+/**
+ * Generates the namespace selector form with hidden attributes.
+ * @param $options Array: the options to be included.
+ */
+function contributionsForm( $options ) {
+       global $wgScript, $wgTitle, $wgRequest;
+
+       $options['title'] = $wgTitle->getPrefixedText();
+       if ( !isset( $options['target'] ) ) {
+               $options['target'] = '';
+       } else {
+               $options['target'] = str_replace( '_' , ' ' , $options['target'] );
+       }
+
+       if ( !isset( $options['namespace'] ) ) {
+               $options['namespace'] = '';
+       }
+
+       if ( !isset( $options['contribs'] ) ) {
+               $options['contribs'] = 'user';
+       }
+
+       if ( !isset( $options['year'] ) ) {
+               $options['year'] = '';
+       }
+
+       if ( !isset( $options['month'] ) ) {
+               $options['month'] = '';
+       }
+
+       if ( $options['contribs'] == 'newbie' ) {
+               $options['target'] = '';
+       }
+
+       $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+
+       foreach ( $options as $name => $value ) {
+               if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
+                       continue;
+               }
+               $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
+       }
+
+       $f .= '<fieldset>' .
+               Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
+               Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ? true : false ) . '<br />' .
+               Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ? true : false ) . ' ' .
+               Xml::input( 'target', 20, $options['target']) . ' '.
+               '<span style="white-space: nowrap">' .
+               Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
+               Xml::namespaceSelector( $options['namespace'], '' ) .
+               '</span>' .
+               Xml::openElement( 'p' ) .
+               '<span style="white-space: nowrap">' .
+               Xml::label( wfMsg( 'year' ), 'year' ) . ' '.
+               Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) .
+               '</span>' .
+               ' '.
+               '<span style="white-space: nowrap">' .
+               Xml::label( wfMsg( 'month' ), 'month' ) . ' '.
+               Xml::monthSelector( $options['month'], -1 ) . ' '.
+               '</span>' .
+               Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
+               Xml::closeElement( 'p' );
+
+       $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' );
+       if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) )
+               $f .= "<p>{$explain}</p>";
+
+       $f .= '</fieldset>' .
+               Xml::closeElement( 'form' );
+       return $f;
+}
diff --git a/includes/specials/Deadendpages.php b/includes/specials/Deadendpages.php
new file mode 100644 (file)
index 0000000..a8416c9
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class DeadendPagesPage extends PageQueryPage {
+
+       function getName( ) {
+               return "Deadendpages";
+       }
+
+       function getPageHeader() {
+               return wfMsgExt( 'deadendpagestext', array( 'parse' ) );
+       }
+
+       /**
+        * LEFT JOIN is expensive
+        *
+        * @return true
+        */
+       function isExpensive( ) {
+               return 1;
+       }
+
+       function isSyndicated() { return false; }
+
+       /**
+        * @return false
+        */
+       function sortDescending() {
+               return false;
+       }
+
+       /**
+        * @return string an sqlquery
+        */
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+               return "SELECT 'Deadendpages' as type, page_namespace AS namespace, page_title as title, page_title AS value " .
+       "FROM $page LEFT JOIN $pagelinks ON page_id = pl_from " .
+       "WHERE pl_from IS NULL " .
+       "AND page_namespace = 0 " .
+       "AND page_is_redirect = 0";
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDeadendpages() {
+
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $depp = new DeadendPagesPage();
+
+       return $depp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Disambiguations.php b/includes/specials/Disambiguations.php
new file mode 100644 (file)
index 0000000..3404566
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class DisambiguationsPage extends PageQueryPage {
+
+       function getName() {
+               return 'Disambiguations';
+       }
+
+       function isExpensive( ) { return true; }
+       function isSyndicated() { return false; }
+
+
+       function getPageHeader( ) {
+               return wfMsgExt( 'disambiguations-text', array( 'parse' ) );
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+
+               $dMsgText = wfMsgForContent('disambiguationspage');
+
+               $linkBatch = new LinkBatch;
+
+               # If the text can be treated as a title, use it verbatim.
+               # Otherwise, pull the titles from the links table
+               $dp = Title::newFromText($dMsgText);
+               if( $dp ) {
+                       if($dp->getNamespace() != NS_TEMPLATE) {
+                               # FIXME we assume the disambiguation message is a template but
+                               # the page can potentially be from another namespace :/
+                               wfDebug("Mediawiki:disambiguationspage message does not refer to a template!\n");
+                       }
+                       $linkBatch->addObj( $dp );
+               } else {
+                               # Get all the templates linked from the Mediawiki:Disambiguationspage
+                               $disPageObj = Title::makeTitleSafe( NS_MEDIAWIKI, 'disambiguationspage' );
+                               $res = $dbr->select(
+                                       array('pagelinks', 'page'),
+                                       'pl_title',
+                                       array('page_id = pl_from', 'pl_namespace' => NS_TEMPLATE,
+                                               'page_namespace' => $disPageObj->getNamespace(), 'page_title' => $disPageObj->getDBkey()),
+                                       __METHOD__ );
+
+                               while ( $row = $dbr->fetchObject( $res ) ) {
+                                       $linkBatch->addObj( Title::makeTitle( NS_TEMPLATE, $row->pl_title ));
+                               }
+
+                               $dbr->freeResult( $res );
+               }
+
+               $set = $linkBatch->constructSet( 'lb.tl', $dbr );
+               if( $set === false ) {
+                       # We must always return a valid sql query, but this way DB will always quicly return an empty result
+                       $set = 'FALSE';
+                       wfDebug("Mediawiki:disambiguationspage message does not link to any templates!\n");
+               }
+
+               list( $page, $pagelinks, $templatelinks) = $dbr->tableNamesN( 'page', 'pagelinks', 'templatelinks' );
+
+               $sql = "SELECT 'Disambiguations' AS \"type\", pb.page_namespace AS namespace,"
+                       ." pb.page_title AS title, la.pl_from AS value"
+                       ." FROM {$templatelinks} AS lb, {$page} AS pb, {$pagelinks} AS la, {$page} AS pa"
+                       ." WHERE $set"  # disambiguation template(s)
+                       .' AND pa.page_id = la.pl_from'
+                       .' AND pa.page_namespace = ' . NS_MAIN  # Limit to just articles in the main namespace
+                       .' AND pb.page_id = lb.tl_from'
+                       .' AND pb.page_namespace = la.pl_namespace'
+                       .' AND pb.page_title = la.pl_title'
+                       .' ORDER BY lb.tl_namespace, lb.tl_title';
+
+               return $sql;
+       }
+
+       function getOrder() {
+               return '';
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgContLang;
+               $title = Title::newFromId( $result->value );
+               $dp = Title::makeTitle( $result->namespace, $result->title );
+
+               $from = $skin->makeKnownLinkObj( $title, '' );
+               $edit = $skin->makeKnownLinkObj( $title, "(".wfMsgHtml("qbedit").")" , 'redirect=no&action=edit' );
+               $arr  = $wgContLang->getArrow();
+               $to   = $skin->makeKnownLinkObj( $dp, '' );
+
+               return "$from $edit $arr $to";
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialDisambiguations() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $sd = new DisambiguationsPage();
+
+       return $sd->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/DoubleRedirects.php b/includes/specials/DoubleRedirects.php
new file mode 100644 (file)
index 0000000..b1bad0c
--- /dev/null
@@ -0,0 +1,103 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page listing redirects to redirecting page.
+ * The software will automatically not follow double redirects, to prevent loops.
+ * @ingroup SpecialPage
+ */
+class DoubleRedirectsPage extends PageQueryPage {
+
+       function getName() {
+               return 'DoubleRedirects';
+       }
+
+       function isExpensive( ) { return true; }
+       function isSyndicated() { return false; }
+
+       function getPageHeader( ) {
+               return wfMsgExt( 'doubleredirectstext', array( 'parse' ) );
+       }
+
+       function getSQLText( &$dbr, $namespace = null, $title = null ) {
+
+               list( $page, $redirect ) = $dbr->tableNamesN( 'page', 'redirect' );
+
+               $limitToTitle = !( $namespace === null && $title === null );
+               $sql = $limitToTitle ? "SELECT" : "SELECT 'DoubleRedirects' as type," ;
+               $sql .=
+                        " pa.page_namespace as namespace, pa.page_title as title," .
+                        " pb.page_namespace as nsb, pb.page_title as tb," .
+                        " pc.page_namespace as nsc, pc.page_title as tc" .
+                  " FROM $redirect AS ra, $redirect AS rb, $page AS pa, $page AS pb, $page AS pc" .
+                  " WHERE ra.rd_from=pa.page_id" .
+                        " AND ra.rd_namespace=pb.page_namespace" .
+                        " AND ra.rd_title=pb.page_title" .
+                        " AND rb.rd_from=pb.page_id" .
+                        " AND rb.rd_namespace=pc.page_namespace" .
+                        " AND rb.rd_title=pc.page_title";
+
+               if( $limitToTitle ) {
+                       $encTitle = $dbr->addQuotes( $title );
+                       $sql .= " AND pa.page_namespace=$namespace" .
+                                       " AND pa.page_title=$encTitle";
+               }
+
+               return $sql;
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               return $this->getSQLText( $dbr );
+       }
+
+       function getOrder() {
+               return '';
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgContLang;
+
+               $fname = 'DoubleRedirectsPage::formatResult';
+               $titleA = Title::makeTitle( $result->namespace, $result->title );
+
+               if ( $result && !isset( $result->nsb ) ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $sql = $this->getSQLText( $dbr, $result->namespace, $result->title );
+                       $res = $dbr->query( $sql, $fname );
+                       if ( $res ) {
+                               $result = $dbr->fetchObject( $res );
+                               $dbr->freeResult( $res );
+                       }
+               }
+               if ( !$result ) {
+                       return '<s>' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . '</s>';
+               }
+
+               $titleB = Title::makeTitle( $result->nsb, $result->tb );
+               $titleC = Title::makeTitle( $result->nsc, $result->tc );
+
+               $linkA = $skin->makeKnownLinkObj( $titleA, '', 'redirect=no' );
+               $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no');
+               $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' );
+               $linkC = $skin->makeKnownLinkObj( $titleC );
+               $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
+
+               return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialDoubleRedirects() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $sdr = new DoubleRedirectsPage();
+
+       return $sdr->doQuery( $offset, $limit );
+
+}
diff --git a/includes/specials/Emailuser.php b/includes/specials/Emailuser.php
new file mode 100644 (file)
index 0000000..c06d7a5
--- /dev/null
@@ -0,0 +1,282 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialEmailuser( $par ) {
+       global $wgRequest, $wgUser, $wgOut;
+
+       $action = $wgRequest->getVal( 'action' );
+       $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+       $targetUser = EmailUserForm::validateEmailTarget( $target );
+       
+       if ( !( $targetUser instanceof User ) ) {
+               $wgOut->showErrorPage( $targetUser[0], $targetUser[1] );
+               return;
+       }
+       
+       $form = new EmailUserForm( $targetUser,
+                       $wgRequest->getText( 'wpText' ),
+                       $wgRequest->getText( 'wpSubject' ),
+                       $wgRequest->getBool( 'wpCCMe' ) );
+       if ( $action == 'success' ) {
+               $form->showSuccess();
+               return;
+       }
+                                       
+       $error = EmailUserForm::getPermissionsError( $wgUser, $wgRequest->getVal( 'wpEditToken' ) );
+       if ( $error ) {
+               switch ( $error[0] ) {
+                       case 'blockedemailuser':
+                               $wgOut->blockedPage();
+                               return;
+                       case 'actionthrottledtext':
+                               $wgOut->rateLimited();
+                               return;
+                       case 'sessionfailure':
+                               $form->showForm();
+                               return;
+                       default:
+                               $wgOut->showErrorPage( $error[0], $error[1] );
+                               return;
+               }
+       }       
+               
+       
+       if ( "submit" == $action && $wgRequest->wasPosted() ) {
+               $result = $form->doSubmit();
+               
+               if ( !is_null( $result ) ) {
+                       $wgOut->addHTML( wfMsg( "usermailererror" ) .
+                                       ' ' . htmlspecialchars( $result->getMessage() ) );
+               } else {
+                       $titleObj = SpecialPage::getTitleFor( "Emailuser" );
+                       $encTarget = wfUrlencode( $form->getTarget()->getName() );
+                       $wgOut->redirect( $titleObj->getFullURL( "target={$encTarget}&action=success" ) );
+               }
+       } else {
+               $form->showForm();
+       }
+}
+
+/**
+ * Implements the Special:Emailuser web interface, and invokes userMailer for sending the email message.
+ * @ingroup SpecialPage
+ */
+class EmailUserForm {
+
+       var $target;
+       var $text, $subject;
+       var $cc_me;     // Whether user requested to be sent a separate copy of their email.
+
+       /**
+        * @param User $target
+        */
+       function EmailUserForm( $target, $text, $subject, $cc_me ) {
+               $this->target = $target;
+               $this->text = $text;
+               $this->subject = $subject;
+               $this->cc_me = $cc_me;
+       }
+
+       function showForm() {
+               global $wgOut, $wgUser;
+               $skin = $wgUser->getSkin();
+
+               $wgOut->setPagetitle( wfMsg( "emailpage" ) );
+               $wgOut->addWikiMsg( "emailpagetext" );
+
+               if ( $this->subject === "" ) {
+                       $this->subject = wfMsgForContent( "defemailsubject" );
+               }
+
+               $emf = wfMsg( "emailfrom" );
+               $senderLink = $skin->makeLinkObj(
+                       $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) );
+               $emt = wfMsg( "emailto" );
+               $recipientLink = $skin->makeLinkObj(
+                       $this->target->getUserPage(), htmlspecialchars( $this->target->getName() ) );
+               $emr = wfMsg( "emailsubject" );
+               $emm = wfMsg( "emailmessage" );
+               $ems = wfMsg( "emailsend" );
+               $emc = wfMsg( "emailccme" );
+               $encSubject = htmlspecialchars( $this->subject );
+
+               $titleObj = SpecialPage::getTitleFor( "Emailuser" );
+               $action = $titleObj->escapeLocalURL( "target=" .
+                       urlencode( $this->target->getName() ) . "&action=submit" );
+               $token = htmlspecialchars( $wgUser->editToken() );
+
+               $wgOut->addHTML( "
+<form id=\"emailuser\" method=\"post\" action=\"{$action}\">
+<table border='0' id='mailheader'><tr>
+<td align='right'>{$emf}:</td>
+<td align='left'><strong>{$senderLink}</strong></td>
+</tr><tr>
+<td align='right'>{$emt}:</td>
+<td align='left'><strong>{$recipientLink}</strong></td>
+</tr><tr>
+<td align='right'>{$emr}:</td>
+<td align='left'>
+<input type='text' size='60' maxlength='200' name=\"wpSubject\" value=\"{$encSubject}\" />
+</td>
+</tr>
+</table>
+<span id='wpTextLabel'><label for=\"wpText\">{$emm}:</label><br /></span>
+<textarea id=\"wpText\" name=\"wpText\" rows='20' cols='80' style=\"width: 100%;\">" . htmlspecialchars( $this->text ) .
+"</textarea>
+" . wfCheckLabel( $emc, 'wpCCMe', 'wpCCMe', $wgUser->getBoolOption( 'ccmeonemails' ) ) . "<br />
+<input type='submit' name=\"wpSend\" value=\"{$ems}\" />
+<input type='hidden' name='wpEditToken' value=\"$token\" />
+</form>\n" );
+
+       }
+
+       /*
+        * Really send a mail. Permissions should have been checked using 
+        * EmailUserForm::getPermissionsError. It is probably also a good idea to
+        * check the edit token and ping limiter in advance.
+        */
+       function doSubmit() {
+               global $wgUser, $wgUserEmailUseReplyTo;
+
+               $to = new MailAddress( $this->target );
+               $from = new MailAddress( $wgUser );
+               $subject = $this->subject;
+
+               if( wfRunHooks( 'EmailUser', array( &$to, &$from, &$subject, &$this->text ) ) ) {
+
+                       if( $wgUserEmailUseReplyTo ) {
+                               // Put the generic wiki autogenerated address in the From:
+                               // header and reserve the user for Reply-To.
+                               //
+                               // This is a bit ugly, but will serve to differentiate
+                               // wiki-borne mails from direct mails and protects against
+                               // SPF and bounce problems with some mailers (see below).
+                               global $wgPasswordSender;
+                               $mailFrom = new MailAddress( $wgPasswordSender );
+                               $replyTo = $from;
+                       } else {
+                               // Put the sending user's e-mail address in the From: header.
+                               //
+                               // This is clean-looking and convenient, but has issues.
+                               // One is that it doesn't as clearly differentiate the wiki mail
+                               // from "directly" sent mails.
+                               //
+                               // Another is that some mailers (like sSMTP) will use the From
+                               // address as the envelope sender as well. For open sites this
+                               // can cause mails to be flunked for SPF violations (since the
+                               // wiki server isn't an authorized sender for various users'
+                               // domains) as well as creating a privacy issue as bounces
+                               // containing the recipient's e-mail address may get sent to
+                               // the sending user.
+                               $mailFrom = $from;
+                               $replyTo = null;
+                       }
+                       
+                       $mailResult = UserMailer::send( $to, $mailFrom, $subject, $this->text, $replyTo );
+
+                       if( WikiError::isError( $mailResult ) ) {
+                               return $mailResult;                     
+                               
+                       } else {
+
+                               // if the user requested a copy of this mail, do this now,
+                               // unless they are emailing themselves, in which case one copy of the message is sufficient.
+                               if ($this->cc_me && $to != $from) {
+                                       $cc_subject = wfMsg('emailccsubject', $this->target->getName(), $subject);
+                                       if( wfRunHooks( 'EmailUser', array( &$from, &$from, &$cc_subject, &$this->text ) ) ) {
+                                               $ccResult = UserMailer::send( $from, $from, $cc_subject, $this->text );
+                                               if( WikiError::isError( $ccResult ) ) {
+                                                       // At this stage, the user's CC mail has failed, but their
+                                                       // original mail has succeeded. It's unlikely, but still, what to do?
+                                                       // We can either show them an error, or we can say everything was fine,
+                                                       // or we can say we sort of failed AND sort of succeeded. Of these options,
+                                                       // simply saying there was an error is probably best.
+                                                       return $ccResult;
+                                               }
+                                       }
+                               }
+
+                               wfRunHooks( 'EmailUserComplete', array( $to, $from, $subject, $this->text ) );
+                               return;
+                       }
+               }
+       }
+
+       function showSuccess( &$user = null ) {
+               global $wgOut;
+               
+               if ( is_null($user) )
+                       $user = $this->target;
+
+               $wgOut->setPagetitle( wfMsg( "emailsent" ) );
+               $wgOut->addHTML( wfMsg( "emailsenttext" ) );
+
+               $wgOut->returnToMain( false, $user->getUserPage() );
+       }
+       
+       function getTarget() {
+               return $this->target;
+       }
+       
+       static function validateEmailTarget ( $target ) {
+               global $wgEnableEmail, $wgEnableUserEmail;
+
+               if( !( $wgEnableEmail && $wgEnableUserEmail ) ) 
+                       return array( "nosuchspecialpage", "nospecialpagetext" );
+               
+               if ( "" == $target ) {
+                       wfDebug( "Target is empty.\n" );
+                       return array( "notargettitle", "notargettext" );
+               }
+       
+               $nt = Title::newFromURL( $target );
+               if ( is_null( $nt ) ) {
+                       wfDebug( "Target is invalid title.\n" );
+                       return array( "notargettitle", "notargettext" );
+               }
+       
+               $nu = User::newFromName( $nt->getText() );
+               if( is_null( $nu ) || !$nu->canReceiveEmail() ) {
+                       wfDebug( "Target is invalid user or can't receive.\n" );
+                       return array( "noemailtitle", "noemailtext" );
+               }
+               
+               return $nu;
+       }
+       static function getPermissionsError ( $user, $editToken ) {
+               if( !$user->canSendEmail() ) {
+                       wfDebug( "User can't send.\n" );
+                       return array( "mailnologin", "mailnologintext" );
+               }
+               
+               if( $user->isBlockedFromEmailuser() ) {
+                       wfDebug( "User is blocked from sending e-mail.\n" );
+                       return array( "blockedemailuser", "" );
+               }
+               
+               if( $user->pingLimiter( 'emailuser' ) ) {
+                       wfDebug( "Ping limiter triggered.\n" ); 
+                       return array( 'actionthrottledtext', '' );
+               }
+               
+               if( !$user->matchEditToken( $editToken ) ) {
+                       wfDebug( "Matching edit token failed.\n" );
+                       return array( 'sessionfailure', '' );
+               }
+               
+               return;
+       }
+       
+       static function newFromURL( $target, $text, $subject, $cc_me )
+       {
+               $nt = Title::newFromURL( $target );
+               $nu = User::newFromName( $nt->getText() );
+               return new EmailUserForm( $nu, $text, $subject, $cc_me );
+       }
+}
diff --git a/includes/specials/Export.php b/includes/specials/Export.php
new file mode 100644 (file)
index 0000000..38bfc83
--- /dev/null
@@ -0,0 +1,284 @@
+<?php
+# Copyright (C) 2003 Brion Vibber <brion@pobox.com>
+# http://www.mediawiki.org/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# http://www.gnu.org/copyleft/gpl.html
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfExportGetPagesFromCategory( $title ) {
+       global $wgContLang;
+
+       $name = $title->getDBkey();
+
+       $dbr = wfGetDB( DB_SLAVE );
+
+       list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+       $sql = "SELECT page_namespace, page_title FROM $page " .
+               "JOIN $categorylinks ON cl_from = page_id " .
+               "WHERE cl_to = " . $dbr->addQuotes( $name );
+
+       $pages = array();
+       $res = $dbr->query( $sql, 'wfExportGetPagesFromCategory' );
+       while ( $row = $dbr->fetchObject( $res ) ) {
+               $n = $row->page_title;
+               if ($row->page_namespace) {
+                       $ns = $wgContLang->getNsText( $row->page_namespace );
+                       $n = $ns . ':' . $n;
+               }
+
+               $pages[] = $n;
+       }
+       $dbr->freeResult($res);
+
+       return $pages;
+}
+
+/**
+ * Expand a list of pages to include templates used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetTemplates( $inputPages, $pageSet ) {
+       return wfExportGetLinks( $inputPages, $pageSet,
+               'templatelinks',
+               array( 'tl_namespace AS namespace', 'tl_title AS title' ),
+               array( 'page_id=tl_from' ) );
+}
+
+/**
+ * Expand a list of pages to include images used in those pages.
+ * @param $inputPages array, list of titles to look up
+ * @param $pageSet array, associative array indexed by titles for output
+ * @return array associative array index by titles
+ */
+function wfExportGetImages( $inputPages, $pageSet ) {
+       return wfExportGetLinks( $inputPages, $pageSet,
+               'imagelinks',
+               array( NS_IMAGE . ' AS namespace', 'il_to AS title' ),
+               array( 'page_id=il_from' ) );
+}
+
+/**
+ * Expand a list of pages to include items used in those pages.
+ * @private
+ */
+function wfExportGetLinks( $inputPages, $pageSet, $table, $fields, $join ) {
+       $dbr = wfGetDB( DB_SLAVE );
+       foreach( $inputPages as $page ) {
+               $title = Title::newFromText( $page );
+               if( $title ) {
+                       $pageSet[$title->getPrefixedText()] = true;
+                       /// @fixme May or may not be more efficient to batch these
+                       ///        by namespace when given multiple input pages.
+                       $result = $dbr->select(
+                               array( 'page', $table ),
+                               $fields,
+                               array_merge( $join,
+                                       array(
+                                               'page_namespace' => $title->getNamespace(),
+                                               'page_title' => $title->getDbKey() ) ),
+                               __METHOD__ );
+                       foreach( $result as $row ) {
+                               $template = Title::makeTitle( $row->namespace, $row->title );
+                               $pageSet[$template->getPrefixedText()] = true;
+                       }
+               }
+       }
+       return $pageSet;
+}
+
+/**
+ * Callback function to remove empty strings from the pages array.
+ */
+function wfFilterPage( $page ) {
+       return $page !== '' && $page !== null;
+}
+
+/**
+ *
+ */
+function wfSpecialExport( $page = '' ) {
+       global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors;
+       global $wgExportAllowHistory, $wgExportMaxHistory;
+
+       $curonly = true;
+       $doexport = false;
+
+       if ( $wgRequest->getCheck( 'addcat' ) ) {
+               $page = $wgRequest->getText( 'pages' );
+               $catname = $wgRequest->getText( 'catname' );
+
+               if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
+                       $t = Title::makeTitleSafe( NS_CATEGORY, $catname );
+                       if ( $t ) {
+                               /**
+                                * @fixme This can lead to hitting memory limit for very large
+                                * categories. Ideally we would do the lookup synchronously
+                                * during the export in a single query.
+                                */
+                               $catpages = wfExportGetPagesFromCategory( $t );
+                               if ( $catpages ) $page .= "\n" . implode( "\n", $catpages );
+                       }
+               }
+       }
+       else if( $wgRequest->wasPosted() && $page == '' ) {
+               $page = $wgRequest->getText( 'pages' );
+               $curonly = $wgRequest->getCheck( 'curonly' );
+               $rawOffset = $wgRequest->getVal( 'offset' );
+               if( $rawOffset ) {
+                       $offset = wfTimestamp( TS_MW, $rawOffset );
+               } else {
+                       $offset = null;
+               }
+               $limit = $wgRequest->getInt( 'limit' );
+               $dir = $wgRequest->getVal( 'dir' );
+               $history = array(
+                       'dir' => 'asc',
+                       'offset' => false,
+                       'limit' => $wgExportMaxHistory,
+               );
+               $historyCheck = $wgRequest->getCheck( 'history' );
+               if ( $curonly ) {
+                       $history = WikiExporter::CURRENT;
+               } elseif ( !$historyCheck ) {
+                       if ( $limit > 0 && $limit < $wgExportMaxHistory ) {
+                               $history['limit'] = $limit;
+                       }
+                       if ( !is_null( $offset ) ) {
+                               $history['offset'] = $offset;
+                       }
+                       if ( strtolower( $dir ) == 'desc' ) {
+                               $history['dir'] = 'desc';
+                       }
+               }
+
+               if( $page != '' ) $doexport = true;
+       } else {
+               // Default to current-only for GET requests
+               $page = $wgRequest->getText( 'pages', $page );
+               $historyCheck = $wgRequest->getCheck( 'history' );
+               if( $historyCheck ) {
+                       $history = WikiExporter::FULL;
+               } else {
+                       $history = WikiExporter::CURRENT;
+               }
+
+               if( $page != '' ) $doexport = true;
+       }
+
+       if( !$wgExportAllowHistory ) {
+               // Override
+               $history = WikiExporter::CURRENT;
+       }
+
+       $list_authors = $wgRequest->getCheck( 'listauthors' );
+       if ( !$curonly || !$wgExportAllowListContributors ) $list_authors = false ;
+
+       if ( $doexport ) {
+               $wgOut->disable();
+
+               // Cancel output buffering and gzipping if set
+               // This should provide safer streaming for pages with history
+               wfResetOutputBuffers();
+               header( "Content-type: application/xml; charset=utf-8" );
+               if( $wgRequest->getCheck( 'wpDownload' ) ) {
+                       // Provide a sane filename suggestion
+                       $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' );
+                       $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" );
+               }
+
+               /* Split up the input and look up linked pages */
+               $inputPages = array_filter( explode( "\n", $page ), 'wfFilterPage' );
+               $pageSet = array_flip( $inputPages );
+
+               if( $wgRequest->getCheck( 'templates' ) ) {
+                       $pageSet = wfExportGetTemplates( $inputPages, $pageSet );
+               }
+
+               /*
+               // Enable this when we can do something useful exporting/importing image information. :)
+               if( $wgRequest->getCheck( 'images' ) ) {
+                       $pageSet = wfExportGetImages( $inputPages, $pageSet );
+               }
+               */
+
+               $pages = array_keys( $pageSet );
+
+               /* Ok, let's get to it... */
+
+               $db = wfGetDB( DB_SLAVE );
+               $exporter = new WikiExporter( $db, $history );
+               $exporter->list_authors = $list_authors ;
+               $exporter->openStream();
+
+               foreach( $pages as $page ) {
+                       /*
+                       if( $wgExportMaxHistory && !$curonly ) {
+                               $title = Title::newFromText( $page );
+                               if( $title ) {
+                                       $count = Revision::countByTitle( $db, $title );
+                                       if( $count > $wgExportMaxHistory ) {
+                                               wfDebug( __FUNCTION__ .
+                                                       ": Skipped $page, $count revisions too big\n" );
+                                               continue;
+                                       }
+                               }
+                       }*/
+
+                       #Bug 8824: Only export pages the user can read
+                       $title = Title::newFromText( $page );
+                       if( is_null( $title ) ) continue; #TODO: perhaps output an <error> tag or something.
+                       if( !$title->userCanRead() ) continue; #TODO: perhaps output an <error> tag or something.
+
+                       $exporter->pageByTitle( $title );
+               }
+
+               $exporter->closeStream();
+               return;
+       }
+
+       $self = SpecialPage::getTitleFor( 'Export' );
+       $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) );
+
+       $form = Xml::openElement( 'form', array( 'method' => 'post',
+               'action' => $self->getLocalUrl( 'action=submit' ) ) );
+
+       $form .= Xml::inputLabel( wfMsg( 'export-addcattext' )  , 'catname', 'catname', 40 ) . '&nbsp;';
+       $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
+
+       $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) );
+       $form .= htmlspecialchars( $page );
+       $form .= Xml::closeElement( 'textarea' );
+       $form .= '<br />';
+
+       if( $wgExportAllowHistory ) {
+               $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '<br />';
+       } else {
+               $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) );
+       }
+       $form .= Xml::checkLabel( wfMsg( 'export-templates' ), 'templates', 'wpExportTemplates', false ) . '<br />';
+       // Enable this when we can do something useful exporting/importing image information. :)
+       //$form .= Xml::checkLabel( wfMsg( 'export-images' ), 'images', 'wpExportImages', false ) . '<br />';
+       $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '<br />';
+
+       $form .= Xml::submitButton( wfMsg( 'export-submit' ) );
+       $form .= Xml::closeElement( 'form' );
+       $wgOut->addHtml( $form );
+}
diff --git a/includes/specials/Fewestrevisions.php b/includes/specials/Fewestrevisions.php
new file mode 100644 (file)
index 0000000..5ad8136
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page for listing the articles with the fewest revisions.
+ *
+ * @ingroup SpecialPage
+ * @author Martin Drashkov
+ */
+class FewestrevisionsPage extends QueryPage {
+
+       function getName() {
+               return 'Fewestrevisions';
+       }
+
+       function isExpensive() {
+               return true;
+       }
+
+       function isSyndicated() {
+               return false;
+       }
+
+       function getSql() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
+
+               return "SELECT 'Fewestrevisions' as type,
+                               page_namespace as namespace,
+                               page_title as title,
+                               COUNT(*) as value
+                       FROM $revision
+                       JOIN $page ON page_id = rev_page
+                       WHERE page_namespace = " . NS_MAIN . "
+                       GROUP BY 1,2,3
+                       HAVING COUNT(*) > 1";
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+
+               $nt = Title::makeTitleSafe( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+               $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+               $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+                       $wgLang->formatNum( $result->value ) );
+               $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
+
+               return wfSpecialList( $plink, $nlink );
+       }
+}
+
+function wfSpecialFewestrevisions() {
+       list( $limit, $offset ) = wfCheckLimits();
+       $frp = new FewestrevisionsPage();
+       $frp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/FileDuplicateSearch.php b/includes/specials/FileDuplicateSearch.php
new file mode 100644 (file)
index 0000000..5236ca2
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/**
+ * A special page to search for files by hash value as defined in the
+ * img_sha1 field in the image table
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Raimond Spekking, based on Special:MIMESearch by Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested hash, comparing this with the
+ * 'img_sha1' field in the image table.
+ * @ingroup SpecialPage
+ */
+class FileDuplicateSearchPage extends QueryPage {
+       var $hash, $filename;
+
+       function FileDuplicateSearchPage( $hash, $filename ) {
+               $this->hash = $hash;
+               $this->filename = $filename;
+       }
+
+       function getName() { return 'FileDuplicateSearch'; }
+       function isExpensive() { return false; }
+       function isSyndicated() { return false; }
+
+       function linkParameters() {
+               return array( 'filename' => $this->filename );
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $image = $dbr->tableName( 'image' );
+               $hash = $dbr->addQuotes( $this->hash );
+
+               return "SELECT 'FileDuplicateSearch' AS type,
+                               img_name AS title,
+                               img_sha1 AS value,
+                               img_user_text,
+                               img_timestamp
+                       FROM $image
+                       WHERE img_sha1 = $hash
+                       ";
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgContLang, $wgLang;
+
+               $nt = Title::makeTitle( NS_IMAGE, $result->title );
+               $text = $wgContLang->convert( $nt->getText() );
+               $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+               $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+               $time = $wgLang->timeanddate( $result->img_timestamp );
+
+               return "$plink . . $user . . $time";
+       }
+}
+
+/**
+ * Output the HTML search form, and constructs the FileDuplicateSearch object.
+ */
+function wfSpecialFileDuplicateSearch( $par = null ) {
+       global $wgRequest, $wgTitle, $wgOut, $wgLang, $wgContLang;
+
+       $hash = '';
+       $filename =  isset( $par ) ?  $par : $wgRequest->getText( 'filename' );
+
+       $title = Title::newFromText( $filename );
+       if( $title && $title->getText() != '' ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               $image = $dbr->tableName( 'image' );
+               $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDbKey() ) );
+               $sql = "SELECT img_sha1 from $image where img_name = $encFilename";
+               $res = $dbr->query( $sql );
+               $row = $dbr->fetchRow( $res );
+               if( $row !== false ) {
+                       $hash = $row[0];
+               }
+               $dbr->freeResult( $res );
+       }
+
+       # Create the input form
+       $wgOut->addHTML(
+               Xml::openElement( 'form', array( 'id' => 'fileduplicatesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+               Xml::openElement( 'fieldset' ) .
+               Xml::element( 'legend', null, wfMsg( 'fileduplicatesearch-legend' ) ) .
+               Xml::inputLabel( wfMsg( 'fileduplicatesearch-filename' ), 'filename', 'filename', 50, $filename ) . ' ' .
+               Xml::submitButton( wfMsg( 'fileduplicatesearch-submit' ) ) .
+               Xml::closeElement( 'fieldset' ) .
+               Xml::closeElement( 'form' )
+       );
+
+       if( $hash != '' ) {
+               $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+               # Show a thumbnail of the file
+               $img = wfFindFile( $title );
+               if ( $img ) {
+                       $thumb = $img->getThumbnail( 120, 120 );
+                       if( $thumb ) {
+                               $wgOut->addHTML( '<div style="float:' . $align . '" id="mw-fileduplicatesearch-icon">' .
+                                       $thumb->toHtml( array( 'desc-link' => false ) ) . '<br />' .
+                                       wfMsgExt( 'fileduplicatesearch-info', array( 'parse' ),
+                                               $wgLang->formatNum( $img->getWidth() ),
+                                               $wgLang->formatNum( $img->getHeight() ),
+                                               $wgLang->formatSize( $img->getSize() ),
+                                               $img->getMimeType()
+                                       ) .
+                                       '</div>' );
+                       }
+               }
+
+               # Do the query
+               $wpp = new FileDuplicateSearchPage( $hash, $filename );
+               list( $limit, $offset ) = wfCheckLimits();
+               $count = $wpp->doQuery( $offset, $limit );
+
+               # Show a short summary
+               if( $count == 1 ) {
+                       $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-1">' .
+                               wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) .
+                               '</p>'
+                       );
+               } elseif ( $count > 1 ) {
+                       $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-n">' .
+                               wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) .
+                               '</p>'
+                       );
+               }
+       }
+}
diff --git a/includes/specials/Filepath.php b/includes/specials/Filepath.php
new file mode 100644 (file)
index 0000000..a2ba3e5
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialFilepath( $par ) {
+       global $wgRequest, $wgOut;
+
+       $file = isset( $par ) ? $par : $wgRequest->getText( 'file' );
+
+       $title = Title::newFromText( $file, NS_IMAGE );
+
+       if ( ! $title instanceof Title || $title->getNamespace() != NS_IMAGE ) {
+               $cform = new FilepathForm( $title );
+               $cform->execute();
+       } else {
+               $file = wfFindFile( $title );
+               if ( $file && $file->exists() ) {
+                       $wgOut->redirect( $file->getURL() );
+               } else {
+                       $wgOut->setStatusCode( 404 );
+                       $cform = new FilepathForm( $title );
+                       $cform->execute();
+               }
+       }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class FilepathForm {
+       var $mTitle;
+
+       function FilepathForm( &$title ) {
+               $this->mTitle =& $title;
+       }
+
+       function execute() {
+               global $wgOut, $wgTitle, $wgScript;
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'specialfilepath' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'filepath' ) ) .
+                       Xml::hidden( 'title', $wgTitle->getPrefixedText() ) .
+                       Xml::inputLabel( wfMsg( 'filepath-page' ), 'file', 'file', 25, is_object( $this->mTitle ) ? $this->mTitle->getText() : '' ) . ' ' .
+                       Xml::submitButton( wfMsg( 'filepath-submit' ) ) . "\n" .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' )
+               );
+       }
+}
diff --git a/includes/specials/Imagelist.php b/includes/specials/Imagelist.php
new file mode 100644 (file)
index 0000000..3d449b5
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialImagelist() {
+       global $wgOut;
+
+       $pager = new ImageListPager;
+
+       $limit = $pager->getForm();
+       $body = $pager->getBody();
+       $nav = $pager->getNavigationBar();
+       $wgOut->addHTML( "$limit<br />\n$body<br />\n$nav" );
+}
+
+/**
+ * @ingroup SpecialPage Pager
+ */
+class ImageListPager extends TablePager {
+       var $mFieldNames = null;
+       var $mQueryConds = array();
+
+       function __construct() {
+               global $wgRequest, $wgMiserMode;
+               if ( $wgRequest->getText( 'sort', 'img_date' ) == 'img_date' ) {
+                       $this->mDefaultDirection = true;
+               } else {
+                       $this->mDefaultDirection = false;
+               }
+               $search = $wgRequest->getText( 'ilsearch' );
+               if ( $search != '' && !$wgMiserMode ) {
+                       $nt = Title::newFromUrl( $search );
+                       if( $nt ) {
+                               $dbr = wfGetDB( DB_SLAVE );
+                               $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+                               $m = str_replace( "%", "\\%", $m );
+                               $m = str_replace( "_", "\\_", $m );
+                               $this->mQueryConds = array( "LOWER(img_name) LIKE '%{$m}%'" );
+                       }
+               }
+
+               parent::__construct();
+       }
+
+       function getFieldNames() {
+               if ( !$this->mFieldNames ) {
+                       $this->mFieldNames = array(
+                               'img_timestamp' => wfMsg( 'imagelist_date' ),
+                               'img_name' => wfMsg( 'imagelist_name' ),
+                               'img_user_text' => wfMsg( 'imagelist_user' ),
+                               'img_size' => wfMsg( 'imagelist_size' ),
+                               'img_description' => wfMsg( 'imagelist_description' ),
+                       );
+               }
+               return $this->mFieldNames;
+       }
+
+       function isFieldSortable( $field ) {
+               static $sortable = array( 'img_timestamp', 'img_name', 'img_size' );
+               return in_array( $field, $sortable );
+       }
+
+       function getQueryInfo() {
+               $fields = $this->getFieldNames();
+               $fields = array_keys( $fields );
+               $fields[] = 'img_user';
+               return array(
+                       'tables' => 'image',
+                       'fields' => $fields,
+                       'conds' => $this->mQueryConds
+               );
+       }
+
+       function getDefaultSort() {
+               return 'img_timestamp';
+       }
+
+       function getStartBody() {
+               # Do a link batch query for user pages
+               if ( $this->mResult->numRows() ) {
+                       $lb = new LinkBatch;
+                       $this->mResult->seek( 0 );
+                       while ( $row = $this->mResult->fetchObject() ) {
+                               if ( $row->img_user ) {
+                                       $lb->add( NS_USER, str_replace( ' ', '_', $row->img_user_text ) );
+                               }
+                       }
+                       $lb->execute();
+               }
+
+               return parent::getStartBody();
+       }
+
+       function formatValue( $field, $value ) {
+               global $wgLang;
+               switch ( $field ) {
+                       case 'img_timestamp':
+                               return $wgLang->timeanddate( $value, true );
+                       case 'img_name':
+                               static $imgfile = null;
+                               if ( $imgfile === null ) $imgfile = wfMsg( 'imgfile' );
+
+                               $name = $this->mCurrentRow->img_name;
+                               $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value );
+                               $image = wfLocalFile( $value );
+                               $url = $image->getURL();
+                               $download = Xml::element('a', array( 'href' => $url ), $imgfile );
+                               return "$link ($download)";
+                       case 'img_user_text':
+                               if ( $this->mCurrentRow->img_user ) {
+                                       $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ),
+                                               htmlspecialchars( $value ) );
+                               } else {
+                                       $link = htmlspecialchars( $value );
+                               }
+                               return $link;
+                       case 'img_size':
+                               return $this->getSkin()->formatSize( $value );
+                       case 'img_description':
+                               return $this->getSkin()->commentBlock( $value );
+               }
+       }
+
+       function getForm() {
+               global $wgRequest, $wgMiserMode;
+               $search = $wgRequest->getText( 'ilsearch' );
+
+               $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-imagelist-form' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'imagelist' ) ) .
+                       Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) );
+
+               if ( !$wgMiserMode ) {
+                       $s .= "<br />\n" .
+                               Xml::inputLabel( wfMsg( 'imagelist_search_for' ), 'ilsearch', 'mw-ilsearch', 20, $search );
+               }
+               $s .= ' ' .
+                       Xml::submitButton( wfMsg( 'table_pager_limit_submit' ) ) ."\n" .
+                       $this->getHiddenFields( array( 'limit', 'ilsearch' ) ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) . "\n";
+               return $s;
+       }
+
+       function getTableClass() {
+               return 'imagelist ' . parent::getTableClass();
+       }
+
+       function getNavClass() {
+               return 'imagelist_nav ' . parent::getNavClass();
+       }
+
+       function getSortHeaderClass() {
+               return 'imagelist_sort ' . parent::getSortHeaderClass();
+       }
+}
diff --git a/includes/specials/Import.php b/includes/specials/Import.php
new file mode 100644 (file)
index 0000000..4c37f1f
--- /dev/null
@@ -0,0 +1,1154 @@
+<?php
+/**
+ * MediaWiki page data importer
+ * Copyright (C) 2003,2005 Brion Vibber <brion@pobox.com>
+ * http://www.mediawiki.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialImport( $page = '' ) {
+       global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources;
+       global $wgImportTargetNamespace;
+
+       $interwiki = false;
+       $namespace = $wgImportTargetNamespace;
+       $frompage = '';
+       $history = true;
+
+       if ( wfReadOnly() ) {
+               $wgOut->readOnlyPage();
+               return;
+       }
+
+       if( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit') {
+               $isUpload = false;
+               $namespace = $wgRequest->getIntOrNull( 'namespace' );
+
+               switch( $wgRequest->getVal( "source" ) ) {
+               case "upload":
+                       $isUpload = true;
+                       if( $wgUser->isAllowed( 'importupload' ) ) {
+                               $source = ImportStreamSource::newFromUpload( "xmlimport" );
+                       } else {
+                               return $wgOut->permissionRequired( 'importupload' );
+                       }
+                       break;
+               case "interwiki":
+                       $interwiki = $wgRequest->getVal( 'interwiki' );
+                       $history = $wgRequest->getCheck( 'interwikiHistory' );
+                       $frompage = $wgRequest->getText( "frompage" );
+                       $source = ImportStreamSource::newFromInterwiki(
+                               $interwiki,
+                               $frompage,
+                               $history );
+                       break;
+               default:
+                       $source = new WikiErrorMsg( "importunknownsource" );
+               }
+
+               if( WikiError::isError( $source ) ) {
+                       $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $source->getMessage() ) );
+               } else {
+                       $wgOut->addWikiMsg( "importstart" );
+
+                       $importer = new WikiImporter( $source );
+                       if( !is_null( $namespace ) ) {
+                               $importer->setTargetNamespace( $namespace );
+                       }
+                       $reporter = new ImportReporter( $importer, $isUpload, $interwiki );
+
+                       $reporter->open();
+                       $result = $importer->doImport();
+                       $resultCount = $reporter->close();
+
+                       if( WikiError::isError( $result ) ) {
+                               # No source or XML parse error
+                               $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $result->getMessage() ) );
+                       } elseif( WikiError::isError( $resultCount ) ) {
+                               # Zero revisions
+                               $wgOut->wrapWikiMsg( '<p class="error">$1</p>', array( 'importfailed', $resultCount->getMessage() ) );
+                       } else {
+                               # Success!
+                               $wgOut->addWikiMsg( 'importsuccess' );
+                       }
+                       $wgOut->addWikiText( '<hr />' );
+               }
+       }
+
+       $action = $wgTitle->getLocalUrl( 'action=submit' );
+
+       if( $wgUser->isAllowed( 'importupload' ) ) {
+               $wgOut->addWikiMsg( "importtext" );
+               $wgOut->addHTML(
+                       Xml::openElement( 'fieldset' ).
+                       Xml::element( 'legend', null, wfMsg( 'import-upload' ) ) .
+                       Xml::openElement( 'form', array( 'enctype' => 'multipart/form-data', 'method' => 'post', 'action' => $action ) ) .
+                       Xml::hidden( 'action', 'submit' ) .
+                       Xml::hidden( 'source', 'upload' ) .
+                       Xml::input( 'xmlimport', 50, '', array( 'type' => 'file' ) ) . ' ' .
+                       Xml::submitButton( wfMsg( 'uploadbtn' ) ) .
+                       Xml::closeElement( 'form' ) .
+                       Xml::closeElement( 'fieldset' )
+               );
+       } else {
+               if( empty( $wgImportSources ) ) {
+                       $wgOut->addWikiMsg( 'importnosources' );
+               }
+       }
+
+       if( !empty( $wgImportSources ) ) {
+               $wgOut->addHTML(
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'importinterwiki' ) ) .
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action ) ) .
+                       wfMsgExt( 'import-interwiki-text', array( 'parse' ) ) .
+                       Xml::hidden( 'action', 'submit' ) .
+                       Xml::hidden( 'source', 'interwiki' ) .
+                       Xml::openElement( 'table', array( 'id' => 'mw-import-table' ) ) .
+                       "<tr>
+                               <td>" .
+                                       Xml::openElement( 'select', array( 'name' => 'interwiki' ) )
+               );
+               foreach( $wgImportSources as $prefix ) {
+                       $selected = ( $interwiki === $prefix ) ? ' selected="selected"' : '';
+                       $wgOut->addHTML( Xml::option( $prefix, $prefix, $selected ) );
+               }
+               $wgOut->addHTML(
+                                       Xml::closeElement( 'select' ) .
+                               "</td>
+                               <td>" .
+                                       Xml::input( 'frompage', 50, $frompage ) .
+                               "</td>
+                       </tr>
+                       <tr>
+                               <td>
+                               </td>
+                               <td>" .
+                                       Xml::checkLabel( wfMsg( 'import-interwiki-history' ), 'interwikiHistory', 'interwikiHistory', $history ) .
+                               "</td>
+                       </tr>
+                       <tr>
+                               <td>
+                               </td>
+                               <td>" .
+                                       Xml::label( wfMsg( 'import-interwiki-namespace' ), 'namespace' ) .
+                                       Xml::namespaceSelector( $namespace, '' ) .
+                               "</td>
+                       </tr>
+                       <tr>
+                               <td>
+                               </td>
+                               <td>" .
+                                       Xml::submitButton( wfMsg( 'import-interwiki-submit' ) ) .
+                               "</td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ).
+                       Xml::closeElement( 'form' ) .
+                       Xml::closeElement( 'fieldset' )
+               );
+       }
+}
+
+/**
+ * Reporting callback
+ * @ingroup SpecialPage
+ */
+class ImportReporter {
+       function __construct( $importer, $upload, $interwiki ) {
+               $importer->setPageOutCallback( array( $this, 'reportPage' ) );
+               $this->mPageCount = 0;
+               $this->mIsUpload = $upload;
+               $this->mInterwiki = $interwiki;
+       }
+
+       function open() {
+               global $wgOut;
+               $wgOut->addHtml( "<ul>\n" );
+       }
+
+       function reportPage( $title, $origTitle, $revisionCount, $successCount ) {
+               global $wgOut, $wgUser, $wgLang, $wgContLang;
+
+               $skin = $wgUser->getSkin();
+
+               $this->mPageCount++;
+
+               $localCount = $wgLang->formatNum( $successCount );
+               $contentCount = $wgContLang->formatNum( $successCount );
+
+               if( $successCount > 0 ) {
+                       $wgOut->addHtml( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
+                               wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
+                               "</li>\n"
+                       );
+
+                       $log = new LogPage( 'import' );
+                       if( $this->mIsUpload ) {
+                               $detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
+                                       $contentCount );
+                               $log->addEntry( 'upload', $title, $detail );
+                       } else {
+                               $interwiki = '[[:' . $this->mInterwiki . ':' .
+                                       $origTitle->getPrefixedText() . ']]';
+                               $detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
+                                       $contentCount, $interwiki );
+                               $log->addEntry( 'interwiki', $title, $detail );
+                       }
+
+                       $comment = $detail; // quick
+                       $dbw = wfGetDB( DB_MASTER );
+                       $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true );
+                       $nullRevision->insertOn( $dbw );
+                       $article = new Article( $title );
+                       # Update page record
+                       $article->updateRevisionOn( $dbw, $nullRevision );
+                       wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) );
+               } else {
+                       $wgOut->addHtml( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
+               }
+       }
+
+       function close() {
+               global $wgOut;
+               if( $this->mPageCount == 0 ) {
+                       $wgOut->addHtml( "</ul>\n" );
+                       return new WikiErrorMsg( "importnopages" );
+               }
+               $wgOut->addHtml( "</ul>\n" );
+
+               return $this->mPageCount;
+       }
+}
+
+/**
+ *
+ * @ingroup SpecialPage
+ */
+class WikiRevision {
+       var $title = null;
+       var $id = 0;
+       var $timestamp = "20010115000000";
+       var $user = 0;
+       var $user_text = "";
+       var $text = "";
+       var $comment = "";
+       var $minor = false;
+
+       function setTitle( $title ) {
+               if( is_object( $title ) ) {
+                       $this->title = $title;
+               } elseif( is_null( $title ) ) {
+                       throw new MWException( "WikiRevision given a null title in import. You may need to adjust \$wgLegalTitleChars." );
+               } else {
+                       throw new MWException( "WikiRevision given non-object title in import." );
+               }
+       }
+
+       function setID( $id ) {
+               $this->id = $id;
+       }
+
+       function setTimestamp( $ts ) {
+               # 2003-08-05T18:30:02Z
+               $this->timestamp = wfTimestamp( TS_MW, $ts );
+       }
+
+       function setUsername( $user ) {
+               $this->user_text = $user;
+       }
+
+       function setUserIP( $ip ) {
+               $this->user_text = $ip;
+       }
+
+       function setText( $text ) {
+               $this->text = $text;
+       }
+
+       function setComment( $text ) {
+               $this->comment = $text;
+       }
+
+       function setMinor( $minor ) {
+               $this->minor = (bool)$minor;
+       }
+
+       function setSrc( $src ) {
+               $this->src = $src;
+       }
+
+       function setFilename( $filename ) {
+               $this->filename = $filename;
+       }
+
+       function setSize( $size ) {
+               $this->size = intval( $size );
+       }
+
+       function getTitle() {
+               return $this->title;
+       }
+
+       function getID() {
+               return $this->id;
+       }
+
+       function getTimestamp() {
+               return $this->timestamp;
+       }
+
+       function getUser() {
+               return $this->user_text;
+       }
+
+       function getText() {
+               return $this->text;
+       }
+
+       function getComment() {
+               return $this->comment;
+       }
+
+       function getMinor() {
+               return $this->minor;
+       }
+
+       function getSrc() {
+               return $this->src;
+       }
+
+       function getFilename() {
+               return $this->filename;
+       }
+
+       function getSize() {
+               return $this->size;
+       }
+
+       function importOldRevision() {
+               $dbw = wfGetDB( DB_MASTER );
+
+               # Sneak a single revision into place
+               $user = User::newFromName( $this->getUser() );
+               if( $user ) {
+                       $userId = intval( $user->getId() );
+                       $userText = $user->getName();
+               } else {
+                       $userId = 0;
+                       $userText = $this->getUser();
+               }
+
+               // avoid memory leak...?
+               $linkCache = LinkCache::singleton();
+               $linkCache->clear();
+
+               $article = new Article( $this->title );
+               $pageId = $article->getId();
+               if( $pageId == 0 ) {
+                       # must create the page...
+                       $pageId = $article->insertOn( $dbw );
+                       $created = true;
+               } else {
+                       $created = false;
+
+                       $prior = Revision::loadFromTimestamp( $dbw, $this->title, $this->timestamp );
+                       if( !is_null( $prior ) ) {
+                               // FIXME: this could fail slightly for multiple matches :P
+                               wfDebug( __METHOD__ . ": skipping existing revision for [[" .
+                                       $this->title->getPrefixedText() . "]], timestamp " .
+                                       $this->timestamp . "\n" );
+                               return false;
+                       }
+               }
+
+               # FIXME: Use original rev_id optionally
+               # FIXME: blah blah blah
+
+               #if( $numrows > 0 ) {
+               #       return wfMsg( "importhistoryconflict" );
+               #}
+
+               # Insert the row
+               $revision = new Revision( array(
+                       'page'       => $pageId,
+                       'text'       => $this->getText(),
+                       'comment'    => $this->getComment(),
+                       'user'       => $userId,
+                       'user_text'  => $userText,
+                       'timestamp'  => $this->timestamp,
+                       'minor_edit' => $this->minor,
+                       ) );
+               $revId = $revision->insertOn( $dbw );
+               $changed = $article->updateIfNewerOn( $dbw, $revision );
+
+               if( $created ) {
+                       wfDebug( __METHOD__ . ": running onArticleCreate\n" );
+                       Article::onArticleCreate( $this->title );
+
+                       wfDebug( __METHOD__ . ": running create updates\n" );
+                       $article->createUpdates( $revision );
+
+               } elseif( $changed ) {
+                       wfDebug( __METHOD__ . ": running onArticleEdit\n" );
+                       Article::onArticleEdit( $this->title );
+
+                       wfDebug( __METHOD__ . ": running edit updates\n" );
+                       $article->editUpdates(
+                               $this->getText(),
+                               $this->getComment(),
+                               $this->minor,
+                               $this->timestamp,
+                               $revId );
+               }
+
+               return true;
+       }
+
+       function importUpload() {
+               wfDebug( __METHOD__ . ": STUB\n" );
+
+               /**
+                       // from file revert...
+                       $source = $this->file->getArchiveVirtualUrl( $this->oldimage );
+                       $comment = $wgRequest->getText( 'wpComment' );
+                       // TODO: Preserve file properties from database instead of reloading from file
+                       $status = $this->file->upload( $source, $comment, $comment );
+                       if( $status->isGood() ) {
+               */
+
+               /**
+                       // from file upload...
+               $this->mLocalFile = wfLocalFile( $nt );
+               $this->mDestName = $this->mLocalFile->getName();
+               //....
+                       $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+                       File::DELETE_SOURCE, $this->mFileProps );
+                       if ( !$status->isGood() ) {
+                               $resultDetails = array( 'internal' => $status->getWikiText() );
+               */
+
+               // @fixme upload() uses $wgUser, which is wrong here
+               // it may also create a page without our desire, also wrong potentially.
+               // and, it will record a *current* upload, but we might want an archive version here
+
+               $file = wfLocalFile( $this->getTitle() );
+               if( !$file ) {
+                       var_dump( $file );
+                       wfDebug( "IMPORT: Bad file. :(\n" );
+                       return false;
+               }
+
+               $source = $this->downloadSource();
+               if( !$source ) {
+                       wfDebug( "IMPORT: Could not fetch remote file. :(\n" );
+                       return false;
+               }
+
+               $status = $file->upload( $source,
+                       $this->getComment(),
+                       $this->getComment(), // Initial page, if none present...
+                       File::DELETE_SOURCE,
+                       false, // props...
+                       $this->getTimestamp() );
+
+               if( $status->isGood() ) {
+                       // yay?
+                       wfDebug( "IMPORT: is ok?\n" );
+                       return true;
+               }
+
+               wfDebug( "IMPORT: is bad? " . $status->getXml() . "\n" );
+               return false;
+
+       }
+
+       function downloadSource() {
+               global $wgEnableUploads;
+               if( !$wgEnableUploads ) {
+                       return false;
+               }
+
+               $tempo = tempnam( wfTempDir(), 'download' );
+               $f = fopen( $tempo, 'wb' );
+               if( !$f ) {
+                       wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
+                       return false;
+               }
+
+               // @fixme!
+               $src = $this->getSrc();
+               $data = Http::get( $src );
+               if( !$data ) {
+                       wfDebug( "IMPORT: couldn't fetch source $src\n" );
+                       fclose( $f );
+                       unlink( $tempo );
+                       return false;
+               }
+
+               fwrite( $f, $data );
+               fclose( $f );
+
+               return $tempo;
+       }
+
+}
+
+/**
+ * implements Special:Import
+ * @ingroup SpecialPage
+ */
+class WikiImporter {
+       var $mDebug = false;
+       var $mSource = null;
+       var $mPageCallback = null;
+       var $mPageOutCallback = null;
+       var $mRevisionCallback = null;
+       var $mUploadCallback = null;
+       var $mTargetNamespace = null;
+       var $lastfield;
+       var $tagStack = array();
+
+       function __construct( $source ) {
+               $this->setRevisionCallback( array( $this, "importRevision" ) );
+               $this->setUploadCallback( array( $this, "importUpload" ) );
+               $this->mSource = $source;
+       }
+
+       function throwXmlError( $err ) {
+               $this->debug( "FAILURE: $err" );
+               wfDebug( "WikiImporter XML error: $err\n" );
+       }
+
+       # --------------
+
+       function doImport() {
+               if( empty( $this->mSource ) ) {
+                       return new WikiErrorMsg( "importnotext" );
+               }
+
+               $parser = xml_parser_create( "UTF-8" );
+
+               # case folding violates XML standard, turn it off
+               xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+               xml_set_object( $parser, $this );
+               xml_set_element_handler( $parser, "in_start", "" );
+
+               $offset = 0; // for context extraction on error reporting
+               do {
+                       $chunk = $this->mSource->readChunk();
+                       if( !xml_parse( $parser, $chunk, $this->mSource->atEnd() ) ) {
+                               wfDebug( "WikiImporter::doImport encountered XML parsing error\n" );
+                               return new WikiXmlError( $parser, wfMsgHtml( 'import-parse-failure' ), $chunk, $offset );
+                       }
+                       $offset += strlen( $chunk );
+               } while( $chunk !== false && !$this->mSource->atEnd() );
+               xml_parser_free( $parser );
+
+               return true;
+       }
+
+       function debug( $data ) {
+               if( $this->mDebug ) {
+                       wfDebug( "IMPORT: $data\n" );
+               }
+       }
+
+       function notice( $data ) {
+               global $wgCommandLineMode;
+               if( $wgCommandLineMode ) {
+                       print "$data\n";
+               } else {
+                       global $wgOut;
+                       $wgOut->addHTML( "<li>" . htmlspecialchars( $data ) . "</li>\n" );
+               }
+       }
+
+       /**
+        * Set debug mode...
+        */
+       function setDebug( $debug ) {
+               $this->mDebug = $debug;
+       }
+
+       /**
+        * Sets the action to perform as each new page in the stream is reached.
+        * @param $callback callback
+        * @return callback
+        */
+       function setPageCallback( $callback ) {
+               $previous = $this->mPageCallback;
+               $this->mPageCallback = $callback;
+               return $previous;
+       }
+
+       /**
+        * Sets the action to perform as each page in the stream is completed.
+        * Callback accepts the page title (as a Title object), a second object
+        * with the original title form (in case it's been overridden into a
+        * local namespace), and a count of revisions.
+        *
+        * @param $callback callback
+        * @return callback
+        */
+       function setPageOutCallback( $callback ) {
+               $previous = $this->mPageOutCallback;
+               $this->mPageOutCallback = $callback;
+               return $previous;
+       }
+
+       /**
+        * Sets the action to perform as each page revision is reached.
+        * @param $callback callback
+        * @return callback
+        */
+       function setRevisionCallback( $callback ) {
+               $previous = $this->mRevisionCallback;
+               $this->mRevisionCallback = $callback;
+               return $previous;
+       }
+
+       /**
+        * Sets the action to perform as each file upload version is reached.
+        * @param $callback callback
+        * @return callback
+        */
+       function setUploadCallback( $callback ) {
+               $previous = $this->mUploadCallback;
+               $this->mUploadCallback = $callback;
+               return $previous;
+       }
+
+       /**
+        * Set a target namespace to override the defaults
+        */
+       function setTargetNamespace( $namespace ) {
+               if( is_null( $namespace ) ) {
+                       // Don't override namespaces
+                       $this->mTargetNamespace = null;
+               } elseif( $namespace >= 0 ) {
+                       // FIXME: Check for validity
+                       $this->mTargetNamespace = intval( $namespace );
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * Default per-revision callback, performs the import.
+        * @param $revision WikiRevision
+        * @private
+        */
+       function importRevision( $revision ) {
+               $dbw = wfGetDB( DB_MASTER );
+               return $dbw->deadlockLoop( array( $revision, 'importOldRevision' ) );
+       }
+
+       /**
+        * Dummy for now...
+        */
+       function importUpload( $revision ) {
+               //$dbw = wfGetDB( DB_MASTER );
+               //return $dbw->deadlockLoop( array( $revision, 'importUpload' ) );
+               return false;
+       }
+
+       /**
+        * Alternate per-revision callback, for debugging.
+        * @param $revision WikiRevision
+        * @private
+        */
+       function debugRevisionHandler( &$revision ) {
+               $this->debug( "Got revision:" );
+               if( is_object( $revision->title ) ) {
+                       $this->debug( "-- Title: " . $revision->title->getPrefixedText() );
+               } else {
+                       $this->debug( "-- Title: <invalid>" );
+               }
+               $this->debug( "-- User: " . $revision->user_text );
+               $this->debug( "-- Timestamp: " . $revision->timestamp );
+               $this->debug( "-- Comment: " . $revision->comment );
+               $this->debug( "-- Text: " . $revision->text );
+       }
+
+       /**
+        * Notify the callback function when a new <page> is reached.
+        * @param $title Title
+        * @private
+        */
+       function pageCallback( $title ) {
+               if( is_callable( $this->mPageCallback ) ) {
+                       call_user_func( $this->mPageCallback, $title );
+               }
+       }
+
+       /**
+        * Notify the callback function when a </page> is closed.
+        * @param $title Title
+        * @param $origTitle Title
+        * @param $revisionCount int
+        * @param $successCount Int: number of revisions for which callback returned true
+        * @private
+        */
+       function pageOutCallback( $title, $origTitle, $revisionCount, $successCount ) {
+               if( is_callable( $this->mPageOutCallback ) ) {
+                       call_user_func( $this->mPageOutCallback, $title, $origTitle,
+                               $revisionCount, $successCount );
+               }
+       }
+
+
+       # XML parser callbacks from here out -- beware!
+       function donothing( $parser, $x, $y="" ) {
+               #$this->debug( "donothing" );
+       }
+
+       function in_start( $parser, $name, $attribs ) {
+               $this->debug( "in_start $name" );
+               if( $name != "mediawiki" ) {
+                       return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
+               }
+               xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+       }
+
+       function in_mediawiki( $parser, $name, $attribs ) {
+               $this->debug( "in_mediawiki $name" );
+               if( $name == 'siteinfo' ) {
+                       xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
+               } elseif( $name == 'page' ) {
+                       $this->push( $name );
+                       $this->workRevisionCount = 0;
+                       $this->workSuccessCount = 0;
+                       $this->uploadCount = 0;
+                       $this->uploadSuccessCount = 0;
+                       xml_set_element_handler( $parser, "in_page", "out_page" );
+               } else {
+                       return $this->throwXMLerror( "Expected <page>, got <$name>" );
+               }
+       }
+       function out_mediawiki( $parser, $name ) {
+               $this->debug( "out_mediawiki $name" );
+               if( $name != "mediawiki" ) {
+                       return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
+               }
+               xml_set_element_handler( $parser, "donothing", "donothing" );
+       }
+
+
+       function in_siteinfo( $parser, $name, $attribs ) {
+               // no-ops for now
+               $this->debug( "in_siteinfo $name" );
+               switch( $name ) {
+               case "sitename":
+               case "base":
+               case "generator":
+               case "case":
+               case "namespaces":
+               case "namespace":
+                       break;
+               default:
+                       return $this->throwXMLerror( "Element <$name> not allowed in <siteinfo>." );
+               }
+       }
+
+       function out_siteinfo( $parser, $name ) {
+               if( $name == "siteinfo" ) {
+                       xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+               }
+       }
+
+
+       function in_page( $parser, $name, $attribs ) {
+               $this->debug( "in_page $name" );
+               switch( $name ) {
+               case "id":
+               case "title":
+               case "restrictions":
+                       $this->appendfield = $name;
+                       $this->appenddata = "";
+                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
+                       xml_set_character_data_handler( $parser, "char_append" );
+                       break;
+               case "revision":
+                       $this->push( "revision" );
+                       if( is_object( $this->pageTitle ) ) {
+                               $this->workRevision = new WikiRevision;
+                               $this->workRevision->setTitle( $this->pageTitle );
+                               $this->workRevisionCount++;
+                       } else {
+                               // Skipping items due to invalid page title
+                               $this->workRevision = null;
+                       }
+                       xml_set_element_handler( $parser, "in_revision", "out_revision" );
+                       break;
+               case "upload":
+                       $this->push( "upload" );
+                       if( is_object( $this->pageTitle ) ) {
+                               $this->workRevision = new WikiRevision;
+                               $this->workRevision->setTitle( $this->pageTitle );
+                               $this->uploadCount++;
+                       } else {
+                               // Skipping items due to invalid page title
+                               $this->workRevision = null;
+                       }
+                       xml_set_element_handler( $parser, "in_upload", "out_upload" );
+                       break;
+               default:
+                       return $this->throwXMLerror( "Element <$name> not allowed in a <page>." );
+               }
+       }
+
+       function out_page( $parser, $name ) {
+               $this->debug( "out_page $name" );
+               $this->pop();
+               if( $name != "page" ) {
+                       return $this->throwXMLerror( "Expected </page>, got </$name>" );
+               }
+               xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
+
+               $this->pageOutCallback( $this->pageTitle, $this->origTitle,
+                       $this->workRevisionCount, $this->workSuccessCount );
+
+               $this->workTitle = null;
+               $this->workRevision = null;
+               $this->workRevisionCount = 0;
+               $this->workSuccessCount = 0;
+               $this->pageTitle = null;
+               $this->origTitle = null;
+       }
+
+       function in_nothing( $parser, $name, $attribs ) {
+               $this->debug( "in_nothing $name" );
+               return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
+       }
+       function char_append( $parser, $data ) {
+               $this->debug( "char_append '$data'" );
+               $this->appenddata .= $data;
+       }
+       function out_append( $parser, $name ) {
+               $this->debug( "out_append $name" );
+               if( $name != $this->appendfield ) {
+                       return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
+               }
+
+               switch( $this->appendfield ) {
+               case "title":
+                       $this->workTitle = $this->appenddata;
+                       $this->origTitle = Title::newFromText( $this->workTitle );
+                       if( !is_null( $this->mTargetNamespace ) && !is_null( $this->origTitle ) ) {
+                               $this->pageTitle = Title::makeTitle( $this->mTargetNamespace,
+                                       $this->origTitle->getDBkey() );
+                       } else {
+                               $this->pageTitle = Title::newFromText( $this->workTitle );
+                       }
+                       if( is_null( $this->pageTitle ) ) {
+                               // Invalid page title? Ignore the page
+                               $this->notice( "Skipping invalid page title '$this->workTitle'" );
+                       } else {
+                               $this->pageCallback( $this->workTitle );
+                       }
+                       break;
+               case "id":
+                       if ( $this->parentTag() == 'revision' ) {
+                               if( $this->workRevision )
+                                       $this->workRevision->setID( $this->appenddata );
+                       }
+                       break;
+               case "text":
+                       if( $this->workRevision )
+                               $this->workRevision->setText( $this->appenddata );
+                       break;
+               case "username":
+                       if( $this->workRevision )
+                               $this->workRevision->setUsername( $this->appenddata );
+                       break;
+               case "ip":
+                       if( $this->workRevision )
+                               $this->workRevision->setUserIP( $this->appenddata );
+                       break;
+               case "timestamp":
+                       if( $this->workRevision )
+                               $this->workRevision->setTimestamp( $this->appenddata );
+                       break;
+               case "comment":
+                       if( $this->workRevision )
+                               $this->workRevision->setComment( $this->appenddata );
+                       break;
+               case "minor":
+                       if( $this->workRevision )
+                               $this->workRevision->setMinor( true );
+                       break;
+               case "filename":
+                       if( $this->workRevision )
+                               $this->workRevision->setFilename( $this->appenddata );
+                       break;
+               case "src":
+                       if( $this->workRevision )
+                               $this->workRevision->setSrc( $this->appenddata );
+                       break;
+               case "size":
+                       if( $this->workRevision )
+                               $this->workRevision->setSize( intval( $this->appenddata ) );
+                       break;
+               default:
+                       $this->debug( "Bad append: {$this->appendfield}" );
+               }
+               $this->appendfield = "";
+               $this->appenddata = "";
+
+               $parent = $this->parentTag();
+               xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
+               xml_set_character_data_handler( $parser, "donothing" );
+       }
+
+       function in_revision( $parser, $name, $attribs ) {
+               $this->debug( "in_revision $name" );
+               switch( $name ) {
+               case "id":
+               case "timestamp":
+               case "comment":
+               case "minor":
+               case "text":
+                       $this->appendfield = $name;
+                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
+                       xml_set_character_data_handler( $parser, "char_append" );
+                       break;
+               case "contributor":
+                       $this->push( "contributor" );
+                       xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+                       break;
+               default:
+                       return $this->throwXMLerror( "Element <$name> not allowed in a <revision>." );
+               }
+       }
+
+       function out_revision( $parser, $name ) {
+               $this->debug( "out_revision $name" );
+               $this->pop();
+               if( $name != "revision" ) {
+                       return $this->throwXMLerror( "Expected </revision>, got </$name>" );
+               }
+               xml_set_element_handler( $parser, "in_page", "out_page" );
+
+               if( $this->workRevision ) {
+                       $ok = call_user_func_array( $this->mRevisionCallback,
+                               array( $this->workRevision, $this ) );
+                       if( $ok ) {
+                               $this->workSuccessCount++;
+                       }
+               }
+       }
+
+       function in_upload( $parser, $name, $attribs ) {
+               $this->debug( "in_upload $name" );
+               switch( $name ) {
+               case "timestamp":
+               case "comment":
+               case "text":
+               case "filename":
+               case "src":
+               case "size":
+                       $this->appendfield = $name;
+                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
+                       xml_set_character_data_handler( $parser, "char_append" );
+                       break;
+               case "contributor":
+                       $this->push( "contributor" );
+                       xml_set_element_handler( $parser, "in_contributor", "out_contributor" );
+                       break;
+               default:
+                       return $this->throwXMLerror( "Element <$name> not allowed in an <upload>." );
+               }
+       }
+
+       function out_upload( $parser, $name ) {
+               $this->debug( "out_revision $name" );
+               $this->pop();
+               if( $name != "upload" ) {
+                       return $this->throwXMLerror( "Expected </upload>, got </$name>" );
+               }
+               xml_set_element_handler( $parser, "in_page", "out_page" );
+
+               if( $this->workRevision ) {
+                       $ok = call_user_func_array( $this->mUploadCallback,
+                               array( $this->workRevision, $this ) );
+                       if( $ok ) {
+                               $this->workUploadSuccessCount++;
+                       }
+               }
+       }
+
+       function in_contributor( $parser, $name, $attribs ) {
+               $this->debug( "in_contributor $name" );
+               switch( $name ) {
+               case "username":
+               case "ip":
+               case "id":
+                       $this->appendfield = $name;
+                       xml_set_element_handler( $parser, "in_nothing", "out_append" );
+                       xml_set_character_data_handler( $parser, "char_append" );
+                       break;
+               default:
+                       $this->throwXMLerror( "Invalid tag <$name> in <contributor>" );
+               }
+       }
+
+       function out_contributor( $parser, $name ) {
+               $this->debug( "out_contributor $name" );
+               $this->pop();
+               if( $name != "contributor" ) {
+                       return $this->throwXMLerror( "Expected </contributor>, got </$name>" );
+               }
+               $parent = $this->parentTag();
+               xml_set_element_handler( $parser, "in_$parent", "out_$parent" );
+       }
+
+       private function push( $name ) {
+               array_push( $this->tagStack, $name );
+               $this->debug( "PUSH $name" );
+       }
+
+       private function pop() {
+               $name = array_pop( $this->tagStack );
+               $this->debug( "POP $name" );
+               return $name;
+       }
+
+       private function parentTag() {
+               $name = $this->tagStack[count( $this->tagStack ) - 1];
+               $this->debug( "PARENT $name" );
+               return $name;
+       }
+
+}
+
+/**
+ * @todo document (e.g. one-sentence class description).
+ * @ingroup SpecialPage
+ */
+class ImportStringSource {
+       function __construct( $string ) {
+               $this->mString = $string;
+               $this->mRead = false;
+       }
+
+       function atEnd() {
+               return $this->mRead;
+       }
+
+       function readChunk() {
+               if( $this->atEnd() ) {
+                       return false;
+               } else {
+                       $this->mRead = true;
+                       return $this->mString;
+               }
+       }
+}
+
+/**
+ * @todo document (e.g. one-sentence class description).
+ * @ingroup SpecialPage
+ */
+class ImportStreamSource {
+       function __construct( $handle ) {
+               $this->mHandle = $handle;
+       }
+
+       function atEnd() {
+               return feof( $this->mHandle );
+       }
+
+       function readChunk() {
+               return fread( $this->mHandle, 32768 );
+       }
+
+       static function newFromFile( $filename ) {
+               $file = @fopen( $filename, 'rt' );
+               if( !$file ) {
+                       return new WikiErrorMsg( "importcantopen" );
+               }
+               return new ImportStreamSource( $file );
+       }
+
+       static function newFromUpload( $fieldname = "xmlimport" ) {
+               $upload =& $_FILES[$fieldname];
+
+               if( !isset( $upload ) || !$upload['name'] ) {
+                       return new WikiErrorMsg( 'importnofile' );
+               }
+               if( !empty( $upload['error'] ) ) {
+                       switch($upload['error']){
+                               case 1: # The uploaded file exceeds the upload_max_filesize directive in php.ini.
+                                       return new WikiErrorMsg( 'importuploaderrorsize' );
+                               case 2: # The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
+                                       return new WikiErrorMsg( 'importuploaderrorsize' );
+                               case 3: # The uploaded file was only partially uploaded
+                                       return new WikiErrorMsg( 'importuploaderrorpartial' );
+                           case 6: #Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
+                               return new WikiErrorMsg( 'importuploaderrortemp' );
+                           # case else: # Currently impossible
+                       }
+
+               }
+               $fname = $upload['tmp_name'];
+               if( is_uploaded_file( $fname ) ) {
+                       return ImportStreamSource::newFromFile( $fname );
+               } else {
+                       return new WikiErrorMsg( 'importnofile' );
+               }
+       }
+
+       static function newFromURL( $url, $method = 'GET' ) {
+               wfDebug( __METHOD__ . ": opening $url\n" );
+               # Use the standard HTTP fetch function; it times out
+               # quicker and sorts out user-agent problems which might
+               # otherwise prevent importing from large sites, such
+               # as the Wikimedia cluster, etc.
+               $data = Http::request( $method, $url );
+               if( $data !== false ) {
+                       $file = tmpfile();
+                       fwrite( $file, $data );
+                       fflush( $file );
+                       fseek( $file, 0 );
+                       return new ImportStreamSource( $file );
+               } else {
+                       return new WikiErrorMsg( 'importcantopen' );
+               }
+       }
+
+       public static function newFromInterwiki( $interwiki, $page, $history=false ) {
+               if( $page == '' ) {
+                       return new WikiErrorMsg( 'import-noarticle' );
+               }
+               $link = Title::newFromText( "$interwiki:Special:Export/$page" );
+               if( is_null( $link ) || $link->getInterwiki() == '' ) {
+                       return new WikiErrorMsg( 'importbadinterwiki' );
+               } else {
+                       $params = $history ? 'history=1' : '';
+                       $url = $link->getFullUrl( $params );
+                       # For interwikis, use POST to avoid redirects.
+                       return ImportStreamSource::newFromURL( $url, "POST" );
+               }
+       }
+}
diff --git a/includes/specials/Ipblocklist.php b/includes/specials/Ipblocklist.php
new file mode 100644 (file)
index 0000000..696c7ef
--- /dev/null
@@ -0,0 +1,427 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ */
+function wfSpecialIpblocklist() {
+       global $wgUser, $wgOut, $wgRequest;
+
+       $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) );
+       $id = $wgRequest->getVal( 'id' );
+       $reason = $wgRequest->getText( 'wpUnblockReason' );
+       $action = $wgRequest->getText( 'action' );
+       $successip = $wgRequest->getVal( 'successip' );
+
+       $ipu = new IPUnblockForm( $ip, $id, $reason );
+
+       if( $action == 'unblock' ) {
+               # Check permissions
+               if( !$wgUser->isAllowed( 'block' ) ) {
+                       $wgOut->permissionRequired( 'block' );
+                       return;
+               }
+               # Check for database lock
+               if( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+               # Show unblock form
+               $ipu->showForm( '' );
+       } elseif( $action == 'submit' && $wgRequest->wasPosted()
+               && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+               # Check permissions
+               if( !$wgUser->isAllowed( 'block' ) ) {
+                       $wgOut->permissionRequired( 'block' );
+                       return;
+               }
+               # Check for database lock
+               if( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+               # Remove blocks and redirect user to success page
+               $ipu->doSubmit();
+       } elseif( $action == 'success' ) {
+               # Inform the user of a successful unblock
+               # (No need to check permissions or locks here,
+               # if something was done, then it's too late!)
+               if ( substr( $successip, 0, 1) == '#' ) {
+                       // A block ID was unblocked
+                       $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) );
+               } else {
+                       // A username/IP was unblocked
+                       $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) );
+               }
+       } else {
+               # Just show the block list
+               $ipu->showList( '' );
+       }
+
+}
+
+/**
+ * implements Special:ipblocklist GUI
+ * @ingroup SpecialPage
+ */
+class IPUnblockForm {
+       var $ip, $reason, $id;
+
+       function IPUnblockForm( $ip, $id, $reason ) {
+               $this->ip = strtr( $ip, '_', ' ' );
+               $this->id = $id;
+               $this->reason = $reason;
+       }
+
+       /**
+        * Generates the unblock form
+        * @param $err string: error message
+        * @return $out string: HTML form
+        */
+       function showForm( $err ) {
+               global $wgOut, $wgUser, $wgSysopUserBans;
+
+               $wgOut->setPagetitle( wfMsg( 'unblockip' ) );
+               $wgOut->addWikiMsg( 'unblockiptext' );
+
+               $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+               $action = $titleObj->getLocalURL( "action=submit" );
+
+               if ( "" != $err ) {
+                       $wgOut->setSubtitle( wfMsg( "formerror" ) );
+                       $wgOut->addWikiText( Xml::tags( 'span', array( 'class' => 'error' ), $err ) . "\n" );
+               }
+
+               $addressPart = false;
+               if ( $this->id ) {
+                       $block = Block::newFromID( $this->id );
+                       if ( $block ) {
+                               $encName = htmlspecialchars( $block->getRedactedName() );
+                               $encId = $this->id;
+                               $addressPart = $encName . Xml::hidden( 'id', $encId );
+                               $ipa = wfMsgHtml( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' );
+                       }
+               }
+               if ( !$addressPart ) {
+                       $addressPart = Xml::input( 'wpUnblockAddress', 40, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) );
+                       $ipa = Xml::label( wfMsg( $wgSysopUserBans ? 'ipadressorusername' : 'ipaddress' ), 'wpUnblockAddress' );
+               }
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'ipb-unblock' ) ) .
+                       Xml::openElement( 'table', array( 'id' => 'mw-unblock-table' ) ).
+                       "<tr>
+                               <td class='mw-label'>
+                                       {$ipa}
+                               </td>
+                               <td class='mw-input'>
+                                       {$addressPart}
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class='mw-label'>" .
+                                       Xml::label( wfMsg( 'ipbreason' ), 'wpUnblockReason' ) .
+                               "</td>
+                               <td class='mw-input'>" .
+                                       Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) .
+                               "</td>
+                       </tr>
+                       <tr>
+                               <td>&nbsp;</td>
+                               <td class='mw-submit'>" .
+                                       Xml::submitButton( wfMsg( 'ipusubmit' ), array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) .
+                               "</td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+
+       }
+
+       const UNBLOCK_SUCCESS = 0; // Success
+       const UNBLOCK_NO_SUCH_ID = 1; // No such block ID
+       const UNBLOCK_USER_NOT_BLOCKED = 2; // IP wasn't blocked
+       const UNBLOCK_BLOCKED_AS_RANGE = 3; // IP is part of a range block
+       const UNBLOCK_UNKNOWNERR = 4; // Unknown error
+
+       /**
+        * Backend code for unblocking. doSubmit() wraps around this.
+        * $range is only used when UNBLOCK_BLOCKED_AS_RANGE is returned, in which
+        * case it contains the range $ip is part of.
+        * @return array array(message key, parameters) on failure, empty array on success
+        */
+
+       static function doUnblock(&$id, &$ip, &$reason, &$range = null)
+       {
+               if ( $id ) {
+                       $block = Block::newFromID( $id );
+                       if ( !$block ) {
+                               return array('ipb_cant_unblock', htmlspecialchars($id));
+                       }
+                       $ip = $block->getRedactedName();
+               } else {
+                       $block = new Block();
+                       $ip = trim( $ip );
+                       if ( substr( $ip, 0, 1 ) == "#" ) {
+                               $id = substr( $ip, 1 );
+                               $block = Block::newFromID( $id );
+                               if( !$block ) {
+                                       return array('ipb_cant_unblock', htmlspecialchars($id));
+                               }
+                               $ip = $block->getRedactedName();
+                       } else {
+                               $block = Block::newFromDB( $ip );
+                               if ( !$block ) {
+                                       return array('ipb_cant_unblock', htmlspecialchars($id));
+                               }
+                               if( $block->mRangeStart != $block->mRangeEnd
+                                               && !strstr( $ip, "/" ) ) {
+                                       /* If the specified IP is a single address, and the block is
+                                        * a range block, don't unblock the range. */
+                                        $range = $block->mAddress;
+                                        return array('ipb_blocked_as_range', $ip, $range);
+                               }
+                       }
+               }
+               // Yes, this is really necessary
+               $id = $block->mId;
+
+               # Delete block
+               if ( !$block->delete() ) {
+                       return array('ipb_cant_unblock', htmlspecialchars($id));
+               }
+
+               # Make log entry
+               $log = new LogPage( 'block' );
+               $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $ip ), $reason );
+               return array();
+       }
+
+       function doSubmit() {
+               global $wgOut;
+               $retval = self::doUnblock($this->id, $this->ip, $this->reason, $range);
+               if(!empty($retval))
+               {
+                       $key = array_shift($retval);
+                       $this->showForm(wfMsgReal($key, $retval));
+                       return;
+               }
+               # Report to the user
+               $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+               $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) );
+               $wgOut->redirect( $success );
+       }
+
+       function showList( $msg ) {
+               global $wgOut, $wgUser;
+
+               $wgOut->setPagetitle( wfMsg( "ipblocklist" ) );
+               if ( "" != $msg ) {
+                       $wgOut->setSubtitle( $msg );
+               }
+
+               // Purge expired entries on one in every 10 queries
+               if ( !mt_rand( 0, 10 ) ) {
+                       Block::purgeExpired();
+               }
+
+               $conds = array();
+               $matches = array();
+               // Is user allowed to see all the blocks?
+               if ( !$wgUser->isAllowed( 'suppress' ) )
+                       $conds['ipb_deleted'] = 0;
+               if ( $this->ip == '' ) {
+                       // No extra conditions
+               } elseif ( substr( $this->ip, 0, 1 ) == '#' ) {
+                       $conds['ipb_id'] = substr( $this->ip, 1 );
+               } elseif ( IP::toUnsigned( $this->ip ) !== false ) {
+                       $conds['ipb_address'] = $this->ip;
+                       $conds['ipb_auto'] = 0;
+               } elseif( preg_match( '/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\\/(\\d{1,2})$/', $this->ip, $matches ) ) {
+                       $conds['ipb_address'] = Block::normaliseRange( $this->ip );
+                       $conds['ipb_auto'] = 0;
+               } else {
+                       $user = User::newFromName( $this->ip );
+                       if ( $user && ( $id = $user->getId() ) != 0 ) {
+                               $conds['ipb_user'] = $id;
+                       } else {
+                               // Uh...?
+                               $conds['ipb_address'] = $this->ip;
+                               $conds['ipb_auto'] = 0;
+                       }
+               }
+
+               $pager = new IPBlocklistPager( $this, $conds );
+               if ( $pager->getNumRows() ) {
+                       $wgOut->addHTML(
+                               $this->searchForm() .
+                               $pager->getNavigationBar() .
+                               Xml::tags( 'ul', null, $pager->getBody() ) .
+                               $pager->getNavigationBar()
+                       );
+               } elseif ( $this->ip != '') {
+                       $wgOut->addHTML( $this->searchForm() );
+                       $wgOut->addWikiMsg( 'ipblocklist-no-results' );
+               } else {
+                       $wgOut->addWikiMsg( 'ipblocklist-empty' );
+               }
+       }
+
+       function searchForm() {
+               global $wgTitle, $wgScript, $wgRequest;
+               return
+                       Xml::tags( 'form', array( 'action' => $wgScript ),
+                               Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
+                               Xml::openElement( 'fieldset' ) .
+                               Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) .
+                               Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) .
+                               '&nbsp;' .
+                               Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) .
+                               Xml::closeElement( 'fieldset' )
+                       );
+       }
+
+       /**
+        * Callback function to output a block
+        */
+       function formatRow( $block ) {
+               global $wgUser, $wgLang;
+
+               wfProfileIn( __METHOD__ );
+
+               static $sk=null, $msg=null;
+
+               if( is_null( $sk ) )
+                       $sk = $wgUser->getSkin();
+               if( is_null( $msg ) ) {
+                       $msg = array();
+                       $keys = array( 'infiniteblock', 'expiringblock', 'unblocklink',
+                               'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' );
+                       foreach( $keys as $key ) {
+                               $msg[$key] = wfMsgHtml( $key );
+                       }
+                       $msg['blocklistline'] = wfMsg( 'blocklistline' );
+               }
+
+               # Prepare links to the blocker's user and talk pages
+               $blocker_id = $block->getBy();
+               $blocker_name = $block->getByName();
+               $blocker = $sk->userLink( $blocker_id, $blocker_name );
+               $blocker .= $sk->userToolLinks( $blocker_id, $blocker_name );
+
+               # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks)
+               if( $block->mAuto ) {
+                       $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy
+               } else {
+                       $target = $sk->userLink( $block->mUser, $block->mAddress )
+                               . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK );
+               }
+
+               $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
+
+               $properties = array();
+               $properties[] = Block::formatExpiry( $block->mExpiry );
+               if ( $block->mAnonOnly ) {
+                       $properties[] = $msg['anononlyblock'];
+               }
+               if ( $block->mCreateAccount ) {
+                       $properties[] = $msg['createaccountblock'];
+               }
+               if (!$block->mEnableAutoblock && $block->mUser ) {
+                       $properties[] = $msg['noautoblockblock'];
+               }
+
+               if ( $block->mBlockEmail && $block->mUser ) {
+                       $properties[] = $msg['emailblock'];
+               }
+
+               $properties = implode( ', ', $properties );
+
+               $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) );
+
+               $unblocklink = '';
+               if ( $wgUser->isAllowed('block') ) {
+                       $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
+                       $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')';
+               }
+
+               $comment = $sk->commentBlock( $block->mReason );
+
+               $s = "{$line} $comment";
+               if ( $block->mHideName )
+                       $s = '<span class="history-deleted">' . $s . '</span>';
+
+               wfProfileOut( __METHOD__ );
+               return "<li>$s $unblocklink</li>\n";
+       }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class IPBlocklistPager extends ReverseChronologicalPager {
+       public $mForm, $mConds;
+
+       function __construct( $form, $conds = array() ) {
+               $this->mForm = $form;
+               $this->mConds = $conds;
+               parent::__construct();
+       }
+
+       function getStartBody() {
+               wfProfileIn( __METHOD__ );
+               # Do a link batch query
+               $this->mResult->seek( 0 );
+               $lb = new LinkBatch;
+
+               /*
+               while ( $row = $this->mResult->fetchObject() ) {
+                       $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+                       $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
+                       $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) );
+                       $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) );
+               }*/
+               # Faster way
+               # Usernames and titles are in fact related by a simple substitution of space -> underscore
+               # The last few lines of Title::secureAndSplit() tell the story.
+               while ( $row = $this->mResult->fetchObject() ) {
+                       $name = str_replace( ' ', '_', $row->ipb_by_text );
+                       $lb->add( NS_USER, $name );
+                       $lb->add( NS_USER_TALK, $name );
+                       $name = str_replace( ' ', '_', $row->ipb_address );
+                       $lb->add( NS_USER, $name );
+                       $lb->add( NS_USER_TALK, $name );
+               }
+               $lb->execute();
+               wfProfileOut( __METHOD__ );
+               return '';
+       }
+
+       function formatRow( $row ) {
+               $block = new Block;
+               $block->initFromRow( $row );
+               return $this->mForm->formatRow( $block );
+       }
+
+       function getQueryInfo() {
+               $conds = $this->mConds;
+               $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+               return array(
+                       'tables' => 'ipblocks',
+                       'fields' => '*',
+                       'conds' => $conds,
+               );
+       }
+
+       function getIndexField() {
+               return 'ipb_timestamp';
+       }
+}
diff --git a/includes/specials/Listgrouprights.php b/includes/specials/Listgrouprights.php
new file mode 100644 (file)
index 0000000..131c060
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * This special page lists all defined user groups and the associated rights.
+ * See also @ref $wgGroupPermissions.
+ *
+ * @ingroup SpecialPage
+ * @author Petr Kadlec <mormegil@centrum.cz>
+ */
+class SpecialListGroupRights extends SpecialPage {
+
+       var $skin;
+
+       /**
+        * Constructor
+        */
+       function __construct() {
+               global $wgUser;
+               parent::__construct( 'Listgrouprights' );
+               $this->skin = $wgUser->getSkin();
+       }
+
+       /**
+        * Show the special page
+        */
+       public function execute( $par ) {
+               global $wgOut, $wgGroupPermissions, $wgImplicitGroups, $wgMessageCache;
+               $wgMessageCache->loadAllMessages();
+
+               $this->setHeaders();
+               $this->outputHeader();
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'table', array( 'class' => 'mw-listgrouprights-table' ) ) .
+                               '<tr>' .
+                                       Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) .
+                                       Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) .
+                               '</tr>'
+               );
+
+               foreach( $wgGroupPermissions as $group => $permissions ) {
+                       $groupname = ( $group == '*' ) ? 'all' : htmlspecialchars( $group ); // Replace * with a more descriptive groupname
+
+                       $msg = wfMsg( 'group-' . $groupname );
+                       if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) {
+                               $groupnameLocalized = $groupname;
+                       } else {
+                               $groupnameLocalized = $msg;
+                       }
+
+                       $msg = wfMsgForContent( 'grouppage-' . $groupname );
+                       if ( wfEmptyMsg( 'grouppage-' . $groupname, $msg ) || $msg == '' ) {
+                               $grouppageLocalized = MWNamespace::getCanonicalName( NS_PROJECT ) . ':' . $groupname;
+                       } else {
+                               $grouppageLocalized = $msg;
+                       }
+
+                       if( $group == '*' ) {
+                               // Do not make a link for the generic * group
+                               $grouppage = $groupnameLocalized;
+                       } else {
+                               $grouppage = $this->skin->makeLink( $grouppageLocalized, $groupnameLocalized );
+                       }
+
+                       if ( !in_array( $group, $wgImplicitGroups ) ) {
+                               $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group );
+                       } else {
+                               // No link to Special:listusers for implicit groups as they are unlistable
+                               $grouplink = '';
+                       }
+
+                       $wgOut->addHTML(
+                               '<tr>
+                                       <td>' .
+                                               $grouppage . $grouplink .
+                                       '</td>
+                                       <td>' .
+                                               self::formatPermissions( $permissions ) .
+                                       '</td>
+                               </tr>'
+                       );
+               }
+               $wgOut->addHTML(
+                       Xml::closeElement( 'table' ) . "\n"
+               );
+       }
+
+       /**
+        * Create a user-readable list of permissions from the given array.
+        *
+        * @param $permissions Array of permission => bool (from $wgGroupPermissions items)
+        * @return string List of all granted permissions, separated by comma separator
+        */
+        private static function formatPermissions( $permissions ) {
+               $r = array();
+               foreach( $permissions as $permission => $granted ) {
+                       if ( $granted ) {
+                               $description = wfMsgHTML( 'listgrouprights-right-display',
+                                       User::getRightDescription($permission),
+                                       $permission
+                               );
+                               $r[] = $description;
+                       }
+               }
+               sort( $r );
+               if( empty( $r ) ) {
+                       return '';
+               } else {
+                       return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
+               }
+       }
+}
diff --git a/includes/specials/Listredirects.php b/includes/specials/Listredirects.php
new file mode 100644 (file)
index 0000000..808aab1
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright Â© 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Special:Listredirects - Lists all the redirects on the wiki.
+ * @ingroup SpecialPage
+ */
+class ListredirectsPage extends QueryPage {
+
+       function getName() { return( 'Listredirects' ); }
+       function isExpensive() { return( true ); }
+       function isSyndicated() { return( false ); }
+       function sortDescending() { return( false ); }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $page = $dbr->tableName( 'page' );
+               $sql = "SELECT 'Listredirects' AS type, page_title AS title, page_namespace AS namespace, 0 AS value FROM $page WHERE page_is_redirect = 1";
+               return( $sql );
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgContLang;
+
+               # Make a link to the redirect itself
+               $rd_title = Title::makeTitle( $result->namespace, $result->title );
+               $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' );
+
+               # Find out where the redirect leads
+               $revision = Revision::newFromTitle( $rd_title );
+               if( $revision ) {
+                       # Make a link to the destination page
+                       $target = Title::newFromRedirect( $revision->getText() );
+                       if( $target ) {
+                               $arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
+                               $targetLink = $skin->makeLinkObj( $target );
+                               return "$rd_link $arr $targetLink";
+                       } else {
+                               return "<s>$rd_link</s>";
+                       }
+               } else {
+                       return "<s>$rd_link</s>";
+               }
+       }
+}
+
+function wfSpecialListredirects() {
+       list( $limit, $offset ) = wfCheckLimits();
+       $lrp = new ListredirectsPage();
+       $lrp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Listusers.php b/includes/specials/Listusers.php
new file mode 100644 (file)
index 0000000..7dba44e
--- /dev/null
@@ -0,0 +1,235 @@
+<?php
+
+# Copyright (C) 2004 Brion Vibber, lcrocker, Tim Starling,
+# Domas Mituzas, Ashar Voultoiz, Jens Frank, Zhengzhu.
+#
+# Â© 2006 Rob Church <robchur@gmail.com>
+#
+# http://www.mediawiki.org/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# http://www.gnu.org/copyleft/gpl.html
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * This class is used to get a list of user. The ones with specials
+ * rights (sysop, bureaucrat, developer) will have them displayed
+ * next to their names.
+ *
+ * @ingroup SpecialPage
+ */
+class UsersPager extends AlphabeticPager {
+
+       function __construct($group=null) {
+               global $wgRequest;
+               $this->requestedGroup = $group != "" ? $group : $wgRequest->getVal( 'group' );
+               $un = $wgRequest->getText( 'username' );
+               $this->requestedUser = '';
+               if ( $un != '' ) {
+                       $username = Title::makeTitleSafe( NS_USER, $un );
+                       if( ! is_null( $username ) ) {
+                               $this->requestedUser = $username->getText();
+                       }
+               }
+               parent::__construct();
+       }
+
+
+       function getIndexField() {
+               return 'user_name';
+       }
+
+       function getQueryInfo() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $conds=array();
+               // don't show hidden names
+               $conds[]='ipb_deleted IS NULL OR ipb_deleted = 0';
+               if ($this->requestedGroup != "") {
+                       $conds['ug_group'] = $this->requestedGroup;
+                       $useIndex = '';
+               } else {
+                       $useIndex = $dbr->useIndexClause('user_name');
+               }
+               if ($this->requestedUser != "") {
+                       $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
+               }
+
+               list ($user,$user_groups,$ipblocks) = $dbr->tableNamesN('user','user_groups','ipblocks');
+
+               $query = array(
+                       'tables' => " $user $useIndex LEFT JOIN $user_groups ON user_id=ug_user
+                               LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_auto=0 ",
+                       'fields' => array('user_name',
+                               'MAX(user_id) AS user_id',
+                               'COUNT(ug_group) AS numgroups',
+                               'MAX(ug_group) AS singlegroup'),
+                       'options' => array('GROUP BY' => 'user_name'),
+                       'conds' => $conds
+               );
+
+               wfRunHooks( 'SpecialListusersQueryInfo', array( $this, &$query ) );
+               return $query;
+       }
+
+       function formatRow( $row ) {
+               $userPage = Title::makeTitle( NS_USER, $row->user_name );
+               $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
+
+               if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) {
+                       $list = array();
+                       foreach( self::getGroups( $row->user_id ) as $group )
+                               $list[] = self::buildGroupLink( $group );
+                       $groups = implode( ', ', $list );
+               } elseif( $row->numgroups == 1 ) {
+                       $groups = self::buildGroupLink( $row->singlegroup );
+               } else {
+                       $groups = '';
+               }
+
+               $item = wfSpecialList( $name, $groups );
+               wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
+               return "<li>{$item}</li>";
+       }
+
+       function getBody() {
+               if (!$this->mQueryDone) {
+                       $this->doQuery();
+               }
+               $batch = new LinkBatch;
+
+               $this->mResult->rewind();
+
+               while ( $row = $this->mResult->fetchObject() ) {
+                       $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
+               }
+               $batch->execute();
+               $this->mResult->rewind();
+               return parent::getBody();
+       }
+
+       function getPageHeader( ) {
+               global $wgScript, $wgRequest;
+               $self = $this->getTitle();
+
+               # Form tag
+               $out  = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+                       '<fieldset>' .
+                       Xml::element( 'legend', array(), wfMsg( 'listusers' ) );
+               $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() );
+
+               # Username field
+               $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' .
+                       Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' ';
+
+               # Group drop-down list
+               $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' .
+                       Xml::openElement('select',  array( 'name' => 'group', 'id' => 'group' ) ) .
+                       Xml::option( wfMsg( 'group-all' ), '' );
+               foreach( $this->getAllGroups() as $group => $groupText )
+                       $out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
+               $out .= Xml::closeElement( 'select' ) . ' ';
+
+               wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
+
+               # Submit button and form bottom
+               if( $this->mLimit )
+                       $out .= Xml::hidden( 'limit', $this->mLimit );
+               $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+               wfRunHooks( 'SpecialListusersHeader', array( $this, &$out ) );
+               $out .= '</fieldset>' .
+                       Xml::closeElement( 'form' );
+
+               return $out;
+       }
+
+       function getAllGroups() {
+               $result = array();
+               foreach( User::getAllGroups() as $group ) {
+                       $result[$group] = User::getGroupName( $group );
+               }
+               return $result;
+       }
+
+       /**
+        * Preserve group and username offset parameters when paging
+        * @return array
+        */
+       function getDefaultQuery() {
+               $query = parent::getDefaultQuery();
+               if( $this->requestedGroup != '' )
+                       $query['group'] = $this->requestedGroup;
+               if( $this->requestedUser != '' )
+                       $query['username'] = $this->requestedUser;
+               wfRunHooks( 'SpecialListusersDefaultQuery', array( $this, &$query ) );
+               return $query;
+       }
+
+       /**
+        * Get a list of groups the specified user belongs to
+        *
+        * @param int $uid
+        * @return array
+        */
+       protected static function getGroups( $uid ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               $groups = array();
+               $res = $dbr->select( 'user_groups', 'ug_group', array( 'ug_user' => $uid ), __METHOD__ );
+               if( $res && $dbr->numRows( $res ) > 0 ) {
+                       while( $row = $dbr->fetchObject( $res ) )
+                               $groups[] = $row->ug_group;
+                       $dbr->freeResult( $res );
+               }
+               return $groups;
+       }
+
+       /**
+        * Format a link to a group description page
+        *
+        * @param string $group
+        * @return string
+        */
+       protected static function buildGroupLink( $group ) {
+               static $cache = array();
+               if( !isset( $cache[$group] ) )
+                       $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
+               return $cache[$group];
+       }
+}
+
+/**
+ * constructor
+ * $par string (optional) A group to list users from
+ */
+function wfSpecialListusers( $par = null ) {
+       global $wgRequest, $wgOut;
+
+       $up = new UsersPager($par);
+
+       # getBody() first to check, if empty
+       $usersbody = $up->getBody();
+       $s = $up->getPageHeader();
+       if( $usersbody ) {
+               $s .=   $up->getNavigationBar();
+               $s .=   '<ul>' . $usersbody . '</ul>';
+               $s .=   $up->getNavigationBar() ;
+       } else {
+               $s .=   '<p>' . wfMsgHTML('listusers-noresult') . '</p>';
+       };
+
+       $wgOut->addHTML( $s );
+}
diff --git a/includes/specials/Lockdb.php b/includes/specials/Lockdb.php
new file mode 100644 (file)
index 0000000..0401922
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialLockdb() {
+       global $wgUser, $wgOut, $wgRequest;
+
+       if( !$wgUser->isAllowed( 'siteadmin' ) ) {
+               $wgOut->permissionRequired( 'siteadmin' );
+               return;
+       }
+
+       # If the lock file isn't writable, we can do sweet bugger all
+       global $wgReadOnlyFile;
+       if( !is_writable( dirname( $wgReadOnlyFile ) ) ) {
+               DBLockForm::notWritable();
+               return;
+       }
+
+       $action = $wgRequest->getVal( 'action' );
+       $f = new DBLockForm();
+
+       if ( 'success' == $action ) {
+               $f->showSuccess();
+       } else if ( 'submit' == $action && $wgRequest->wasPosted() &&
+               $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+               $f->doSubmit();
+       } else {
+               $f->showForm( '' );
+       }
+}
+
+/**
+ * A form to make the database readonly (eg for maintenance purposes).
+ * @ingroup SpecialPage
+ */
+class DBLockForm {
+       var $reason = '';
+
+       function DBLockForm() {
+               global $wgRequest;
+               $this->reason = $wgRequest->getText( 'wpLockReason' );
+       }
+
+       function showForm( $err ) {
+               global $wgOut, $wgUser;
+
+               $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+               $wgOut->addWikiMsg( 'lockdbtext' );
+
+               if ( "" != $err ) {
+                       $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+                       $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+               }
+               $lc = htmlspecialchars( wfMsg( 'lockconfirm' ) );
+               $lb = htmlspecialchars( wfMsg( 'lockbtn' ) );
+               $elr = htmlspecialchars( wfMsg( 'enterlockreason' ) );
+               $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+               $action = $titleObj->escapeLocalURL( 'action=submit' );
+               $reason = htmlspecialchars( $this->reason );
+               $token = htmlspecialchars( $wgUser->editToken() );
+
+               $wgOut->addHTML( <<<END
+<form id="lockdb" method="post" action="{$action}">
+{$elr}:
+<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea>
+<table border="0">
+       <tr>
+               <td align="right">
+                       <input type="checkbox" name="wpLockConfirm" />
+               </td>
+               <td align="left">{$lc}</td>
+       </tr>
+       <tr>
+               <td>&nbsp;</td>
+               <td align="left">
+                       <input type="submit" name="wpLock" value="{$lb}" />
+               </td>
+       </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+       }
+
+       function doSubmit() {
+               global $wgOut, $wgUser, $wgLang, $wgRequest;
+               global $wgReadOnlyFile;
+
+               if ( ! $wgRequest->getCheck( 'wpLockConfirm' ) ) {
+                       $this->showForm( wfMsg( 'locknoconfirm' ) );
+                       return;
+               }
+               $fp = @fopen( $wgReadOnlyFile, 'w' );
+
+               if ( false === $fp ) {
+                       # This used to show a file not found error, but the likeliest reason for fopen()
+                       # to fail at this point is insufficient permission to write to the file...good old
+                       # is_writable() is plain wrong in some cases, it seems...
+                       self::notWritable();
+                       return;
+               }
+               fwrite( $fp, $this->reason );
+               fwrite( $fp, "\n<p>(by " . $wgUser->getName() . " at " .
+                 $wgLang->timeanddate( wfTimestampNow() ) . ")\n" );
+               fclose( $fp );
+
+               $titleObj = SpecialPage::getTitleFor( 'Lockdb' );
+               $wgOut->redirect( $titleObj->getFullURL( 'action=success' ) );
+       }
+
+       function showSuccess() {
+               global $wgOut;
+
+               $wgOut->setPagetitle( wfMsg( 'lockdb' ) );
+               $wgOut->setSubtitle( wfMsg( 'lockdbsuccesssub' ) );
+               $wgOut->addWikiMsg( 'lockdbsuccesstext' );
+       }
+
+       public static function notWritable() {
+               global $wgOut;
+               $wgOut->showErrorPage( 'lockdb', 'lockfilenotwritable' );
+       }
+}
diff --git a/includes/specials/Log.php b/includes/specials/Log.php
new file mode 100644 (file)
index 0000000..3154ed1
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+# Copyright (C) 2008 Aaron Schulz
+# http://www.mediawiki.org/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# http://www.gnu.org/copyleft/gpl.html
+
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialLog( $par = '' ) {
+       global $wgRequest, $wgOut, $wgUser;
+       # Get parameters
+       $type = $wgRequest->getVal( 'type', $par );
+       $user = $wgRequest->getText( 'user' );
+       $title = $wgRequest->getText( 'page' );
+       $pattern = $wgRequest->getBool( 'pattern' );
+       $y = $wgRequest->getIntOrNull( 'year' );
+       $m = $wgRequest->getIntOrNull( 'month' );
+       # Don't let the user get stuck with a certain date
+       $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
+       if( $skip ) {
+               $y = '';
+               $m = '';
+       }
+       # Create a LogPager item to get the results and a LogEventsList
+       # item to format them...
+       $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
+       $pager = new LogPager( $loglist, $type, $user, $title, $pattern, array(), $y, $m );
+       # Set title and add header
+       $loglist->showHeader( $pager->getType() );
+       # Show form options
+       $loglist->showOptions( $pager->getType(), $pager->getUser(), $pager->getPage(), $pager->getPattern(),
+               $pager->getYear(), $pager->getMonth() );
+       # Insert list
+       $logBody = $pager->getBody();
+       if( $logBody ) {
+               $wgOut->addHTML(
+                       $pager->getNavigationBar() .
+                       $loglist->beginLogEventsList() .
+                       $logBody .
+                       $loglist->endLogEventsList() .
+                       $pager->getNavigationBar()
+               );
+       } else {
+               $wgOut->addWikiMsg( 'logempty' );
+       }
+}
diff --git a/includes/specials/Lonelypages.php b/includes/specials/Lonelypages.php
new file mode 100644 (file)
index 0000000..5aafac7
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page looking for articles with no article linking to them,
+ * thus being lonely.
+ * @ingroup SpecialPage
+ */
+class LonelyPagesPage extends PageQueryPage {
+
+       function getName() {
+               return "Lonelypages";
+       }
+       function getPageHeader() {
+               return wfMsgExt( 'lonelypagestext', array( 'parse' ) );
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function isExpensive() {
+               return true;
+       }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $pagelinks ) = $dbr->tableNamesN( 'page', 'pagelinks' );
+
+               return
+                 "SELECT 'Lonelypages'  AS type,
+                         page_namespace AS namespace,
+                         page_title     AS title,
+                         page_title     AS value
+                    FROM $page
+               LEFT JOIN $pagelinks
+                      ON page_namespace=pl_namespace AND page_title=pl_title
+                   WHERE pl_namespace IS NULL
+                     AND page_namespace=".NS_MAIN."
+                     AND page_is_redirect=0";
+
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialLonelypages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $lpp = new LonelyPagesPage();
+
+       return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Longpages.php b/includes/specials/Longpages.php
new file mode 100644 (file)
index 0000000..be16a02
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ * @ingroup SpecialPage
+ */
+class LongPagesPage extends ShortPagesPage {
+
+       function getName() {
+               return "Longpages";
+       }
+
+       function sortDescending() {
+               return true;
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialLongpages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $lpp = new LongPagesPage();
+
+       $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/MIMEsearch.php b/includes/specials/MIMEsearch.php
new file mode 100644 (file)
index 0000000..82ee4be
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+/**
+ * A special page to search for files by MIME type as defined in the
+ * img_major_mime and img_minor_mime fields in the image table
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * Searches the database for files of the requested MIME type, comparing this with the
+ * 'img_major_mime' and 'img_minor_mime' fields in the image table.
+ * @ingroup SpecialPage
+ */
+class MIMEsearchPage extends QueryPage {
+       var $major, $minor;
+
+       function MIMEsearchPage( $major, $minor ) {
+               $this->major = $major;
+               $this->minor = $minor;
+       }
+
+       function getName() { return 'MIMEsearch'; }
+
+       /**
+        * Due to this page relying upon extra fields being passed in the SELECT it
+        * will fail if it's set as expensive and misermode is on
+        */
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       function linkParameters() {
+               $arr = array( $this->major, $this->minor );
+               $mime = implode( '/', $arr );
+               return array( 'mime' => $mime );
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $image = $dbr->tableName( 'image' );
+               $major = $dbr->addQuotes( $this->major );
+               $minor = $dbr->addQuotes( $this->minor );
+
+               return
+                       "SELECT 'MIMEsearch' AS type,
+                               " . NS_IMAGE . " AS namespace,
+                               img_name AS title,
+                               img_major_mime AS value,
+
+                               img_size,
+                               img_width,
+                               img_height,
+                               img_user_text,
+                               img_timestamp
+                       FROM $image
+                       WHERE img_major_mime = $major AND img_minor_mime = $minor
+                       ";
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgContLang, $wgLang;
+
+               $nt = Title::makeTitle( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getText() );
+               $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+
+               $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) );
+               $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'),
+                       $wgLang->formatNum( $result->img_size ) );
+               $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ),
+                       $wgLang->formatNum( $result->img_height ) );
+               $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+               $time = $wgLang->timeanddate( $result->img_timestamp );
+
+               return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
+       }
+}
+
+/**
+ * Output the HTML search form, and constructs the MIMEsearchPage object.
+ */
+function wfSpecialMIMEsearch( $par = null ) {
+       global $wgRequest, $wgTitle, $wgOut;
+
+       $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
+
+       $wgOut->addHTML(
+               Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+               Xml::openElement( 'fieldset' ) .
+               Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
+               Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
+               Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
+               Xml::closeElement( 'fieldset' ) .
+               Xml::closeElement( 'form' )
+       );
+
+       list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime );
+       if ( $major == '' or $minor == '' or !wfSpecialMIMEsearchValidType( $major ) )
+               return;
+       $wpp = new MIMEsearchPage( $major, $minor );
+
+       list( $limit, $offset ) = wfCheckLimits();
+       $wpp->doQuery( $offset, $limit );
+}
+
+function wfSpecialMIMEsearchParse( $str ) {
+       // searched for an invalid MIME type.
+       if( strpos( $str, '/' ) === false) {
+               return array ('', '');
+       }
+
+       list( $major, $minor ) = explode( '/', $str, 2 );
+
+       return array(
+               ltrim( $major, ' ' ),
+               rtrim( $minor, ' ' )
+       );
+}
+
+function wfSpecialMIMEsearchValidType( $type ) {
+       // From maintenance/tables.sql => img_major_mime
+       $types = array(
+               'unknown',
+               'application',
+               'audio',
+               'image',
+               'text',
+               'video',
+               'message',
+               'model',
+               'multipart'
+       );
+
+       return in_array( $type, $types );
+}
diff --git a/includes/specials/MergeHistory.php b/includes/specials/MergeHistory.php
new file mode 100644 (file)
index 0000000..6183374
--- /dev/null
@@ -0,0 +1,451 @@
+<?php
+/**
+ * Special page allowing users with the appropriate permissions to
+ * merge article histories, with some restrictions
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMergehistory( $par ) {
+       global $wgRequest;
+
+       $form = new MergehistoryForm( $wgRequest, $par );
+       $form->execute();
+}
+
+/**
+ * The HTML form for Special:MergeHistory, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class MergehistoryForm {
+       var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;
+       var $mTargetObj, $mDestObj;
+
+       function MergehistoryForm( $request, $par = "" ) {
+               global $wgUser;
+
+               $this->mAction = $request->getVal( 'action' );
+               $this->mTarget = $request->getVal( 'target' );
+               $this->mDest = $request->getVal( 'dest' );
+               $this->mSubmitted = $request->getBool( 'submitted' );
+
+               $this->mTargetID = intval( $request->getVal( 'targetID' ) );
+               $this->mDestID = intval( $request->getVal( 'destID' ) );
+               $this->mTimestamp = $request->getVal( 'mergepoint' );
+               if( !preg_match("/[0-9]{14}/",$this->mTimestamp) ) {
+                       $this->mTimestamp = '';
+               }
+               $this->mComment = $request->getText( 'wpComment' );
+
+               $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+               // target page
+               if( $this->mSubmitted ) {
+                       $this->mTargetObj = Title::newFromURL( $this->mTarget );
+                       $this->mDestObj = Title::newFromURL( $this->mDest );
+               } else {
+                       $this->mTargetObj = null;
+                       $this->mDestObj = null;
+               }
+
+               $this->preCacheMessages();
+       }
+
+       /**
+        * As we use the same small set of messages in various methods and that
+        * they are called often, we call them once and save them in $this->message
+        */
+       function preCacheMessages() {
+               // Precache various messages
+               if( !isset( $this->message ) ) {
+                       $this->message['last'] = wfMsgExt( 'last', array( 'escape') );
+               }
+       }
+
+       function execute() {
+               global $wgOut, $wgUser;
+
+               $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
+
+               if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {
+                       return $this->merge();
+               }
+
+               if ( !$this->mSubmitted ) {
+                       $this->showMergeForm();
+                       return;
+               }
+
+               $errors = array();
+               if ( !$this->mTargetObj instanceof Title ) {
+                       $errors[] = wfMsgExt( 'mergehistory-invalid-source', array( 'parse' ) );
+               } elseif( !$this->mTargetObj->exists() ) {
+                       $errors[] = wfMsgExt( 'mergehistory-no-source', array( 'parse' ),
+                               wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
+                       );
+               }
+
+               if ( !$this->mDestObj instanceof Title) {
+                       $errors[] = wfMsgExt( 'mergehistory-invalid-destination', array( 'parse' ) );
+               } elseif( !$this->mDestObj->exists() ) {
+                       $errors[] = wfMsgExt( 'mergehistory-no-destination', array( 'parse' ),
+                               wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
+                       );
+               }
+
+               // TODO: warn about target = dest?
+
+               if ( count( $errors ) ) {
+                       $this->showMergeForm();
+                       $wgOut->addHTML( implode( "\n", $errors ) );
+               } else {
+                       $this->showHistory();
+               }
+
+       }
+
+       function showMergeForm() {
+               global $wgOut, $wgScript;
+
+               $wgOut->addWikiMsg( 'mergehistory-header' );
+
+               $wgOut->addHtml(
+                       Xml::openElement( 'form', array(
+                               'method' => 'get',
+                               'action' => $wgScript ) ) .
+                       '<fieldset>' .
+                       Xml::element( 'legend', array(),
+                               wfMsg( 'mergehistory-box' ) ) .
+                       Xml::hidden( 'title',
+                               SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .
+                       Xml::hidden( 'submitted', '1' ) .
+                       Xml::hidden( 'mergepoint', $this->mTimestamp ) .
+                       Xml::openElement( 'table' ) .
+                       "<tr>
+                               <td>".Xml::label( wfMsg( 'mergehistory-from' ), 'target' )."</td>
+                               <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>
+                       </tr><tr>
+                               <td>".Xml::label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>
+                               <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>
+                       </tr><tr><td>" .
+                       Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .
+                       "</td></tr>" .
+                       Xml::closeElement( 'table' ) .
+                       '</fieldset>' .
+                       '</form>' );
+       }
+
+       private function showHistory() {
+               global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+               $this->sk = $wgUser->getSkin();
+
+               $wgOut->setPagetitle( wfMsg( "mergehistory" ) );
+
+               $this->showMergeForm();
+
+               # List all stored revisions
+               $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );
+               $haveRevisions = $revisions && $revisions->getNumRows() > 0;
+
+               $titleObj = SpecialPage::getTitleFor( "Mergehistory" );
+               $action = $titleObj->getLocalURL( "action=submit" );
+               # Start the form here
+               $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
+               $wgOut->addHtml( $top );
+
+               if( $haveRevisions ) {
+                       # Format the user-visible controls (comment field, submission button)
+                       # in a nice little table
+                       $align = $wgContLang->isRtl() ? 'left' : 'right';
+                       $table =
+                               Xml::openElement( 'fieldset' ) .
+                               Xml::openElement( 'table' ) .
+                                       "<tr>
+                                               <td colspan='2'>" .
+                                                       wfMsgExt( 'mergehistory-merge', array('parseinline'),
+                                                               $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .
+                                               "</td>
+                                       </tr>
+                                       <tr>
+                                               <td align='$align'>" .
+                                                       Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+                                               "</td>
+                                               <td>" .
+                                                       Xml::input( 'wpComment', 50, $this->mComment ) .
+                                               "</td>
+                                       </tr>
+                                       <tr>
+                                               <td>&nbsp;</td>
+                                               <td>" .
+                                                       Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .
+                                               "</td>
+                                       </tr>" .
+                               Xml::closeElement( 'table' ) .
+                               Xml::closeElement( 'fieldset' );
+
+                       $wgOut->addHtml( $table );
+               }
+
+               $wgOut->addHTML( "<h2 id=\"mw-mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );
+
+               if( $haveRevisions ) {
+                       $wgOut->addHTML( $revisions->getNavigationBar() );
+                       $wgOut->addHTML( "<ul>" );
+                       $wgOut->addHTML( $revisions->getBody() );
+                       $wgOut->addHTML( "</ul>" );
+                       $wgOut->addHTML( $revisions->getNavigationBar() );
+               } else {
+                       $wgOut->addWikiMsg( "mergehistory-empty" );
+               }
+
+               # Show relevant lines from the deletion log:
+               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );
+               LogEventsList::showLogExtract( $wgOut, 'merge', $this->mTargetObj->getPrefixedText() );
+
+               # When we submit, go by page ID to avoid some nasty but unlikely collisions.
+               # Such would happen if a page was renamed after the form loaded, but before submit
+               $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );
+               $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );
+               $misc .= Xml::hidden( 'target', $this->mTarget );
+               $misc .= Xml::hidden( 'dest', $this->mDest );
+               $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+               $misc .= Xml::closeElement( 'form' );
+               $wgOut->addHtml( $misc );
+
+               return true;
+       }
+
+       function formatRevisionRow( $row ) {
+               global $wgUser, $wgLang;
+
+               $rev = new Revision( $row );
+
+               $stxt = '';
+               $last = $this->message['last'];
+
+               $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
+               $checkBox = wfRadio( "mergepoint", $ts, false );
+
+               $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
+                       htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() );
+               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+                       $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+               }
+
+               # Last link
+               if( !$rev->userCan( Revision::DELETED_TEXT ) )
+                       $last = $this->message['last'];
+               else if( isset($this->prevId[$row->rev_id]) )
+                       $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'],
+                               "diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] );
+
+               $userLink = $this->sk->revUserTools( $rev );
+
+               if(!is_null($size = $row->rev_len)) {
+                       if($size == 0)
+                               $stxt = wfMsgHtml('historyempty');
+                       else
+                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
+               }
+               $comment = $this->sk->revComment( $rev );
+
+               return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";
+       }
+
+       /**
+        * Fetch revision text link if it's available to all users
+        * @return string
+        */
+       function getPageLink( $row, $titleObj, $ts, $target ) {
+               global $wgLang;
+
+               if( !$this->userCan($row, Revision::DELETED_TEXT) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $this->sk->makeKnownLinkObj( $titleObj,
+                               $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
+                       if( $this->isDeleted($row, Revision::DELETED_TEXT) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       function merge() {
+               global $wgOut, $wgUser;
+               # Get the titles directly from the IDs, in case the target page params
+               # were spoofed. The queries are done based on the IDs, so it's best to
+               # keep it consistent...
+               $targetTitle = Title::newFromID( $this->mTargetID );
+               $destTitle = Title::newFromID( $this->mDestID );
+               if( is_null($targetTitle) || is_null($destTitle) )
+                       return false; // validate these
+               if( $targetTitle->getArticleId() == $destTitle->getArticleId() )
+                       return false;
+               # Verify that this timestamp is valid
+               # Must be older than the destination page
+               $dbw = wfGetDB( DB_MASTER );
+               # Get timestamp into DB format
+               $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp($this->mTimestamp) : '';
+               # Max timestamp should be min of destination page
+               $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',
+                       array('rev_page' => $this->mDestID ),
+                       __METHOD__ );
+               # Destination page must exist with revisions
+               if( !$maxtimestamp ) {
+                       $wgOut->addWikiMsg('mergehistory-fail');
+                       return false;
+               }
+               # Get the latest timestamp of the source
+               $lasttimestamp = $dbw->selectField( array('page','revision'),
+                       'rev_timestamp',
+                       array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),
+                       __METHOD__ );
+               # $this->mTimestamp must be older than $maxtimestamp
+               if( $this->mTimestamp >= $maxtimestamp ) {
+                       $wgOut->addWikiMsg('mergehistory-fail');
+                       return false;
+               }
+               # Update the revisions
+               if( $this->mTimestamp ) {
+                       $timewhere = "rev_timestamp <= {$this->mTimestamp}";
+                       $TimestampLimit = wfTimestamp(TS_MW,$this->mTimestamp);
+               } else {
+                       $timewhere = "rev_timestamp <= {$maxtimestamp}";
+                       $TimestampLimit = wfTimestamp(TS_MW,$lasttimestamp);
+               }
+               # Do the moving...
+               $dbw->update( 'revision',
+                       array( 'rev_page' => $this->mDestID ),
+                       array( 'rev_page' => $this->mTargetID,
+                               $timewhere ),
+                       __METHOD__ );
+
+               $count = $dbw->affectedRows();
+               # Make the source page a redirect if no revisions are left
+               $haveRevisions = $dbw->selectField( 'revision',
+                       'rev_timestamp',
+                       array( 'rev_page' => $this->mTargetID  ),
+                       __METHOD__,
+                       array( 'FOR UPDATE' ) );
+               if( !$haveRevisions ) {
+                       if( $this->mComment ) {
+                               $comment = wfMsgForContent( 'mergehistory-comment', $targetTitle->getPrefixedText(),
+                                       $destTitle->getPrefixedText(), $this->mComment );
+                       } else {
+                               $comment = wfMsgForContent( 'mergehistory-autocomment', $targetTitle->getPrefixedText(),
+                                       $destTitle->getPrefixedText() );
+                       }
+                       $mwRedir = MagicWord::get( 'redirect' );
+                       $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $destTitle->getPrefixedText() . "]]\n";
+                       $redirectArticle = new Article( $targetTitle );
+                       $redirectRevision = new Revision( array(
+                               'page'    => $this->mTargetID,
+                               'comment' => $comment,
+                               'text'    => $redirectText ) );
+                       $redirectRevision->insertOn( $dbw );
+                       $redirectArticle->updateRevisionOn( $dbw, $redirectRevision );
+
+                       # Now, we record the link from the redirect to the new title.
+                       # It should have no other outgoing links...
+                       $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
+                       $dbw->insert( 'pagelinks',
+                               array(
+                                       'pl_from'      => $this->mDestID,
+                                       'pl_namespace' => $destTitle->getNamespace(),
+                                       'pl_title'     => $destTitle->getDBkey() ),
+                               __METHOD__ );
+               } else {
+                       $targetTitle->invalidateCache(); // update histories
+               }
+               $destTitle->invalidateCache(); // update histories
+               # Check if this did anything
+               if( !$count ) {
+                       $wgOut->addWikiMsg('mergehistory-fail');
+                       return false;
+               }
+               # Update our logs
+               $log = new LogPage( 'merge' );
+               $log->addEntry( 'merge', $targetTitle, $this->mComment,
+                       array($destTitle->getPrefixedText(),$TimestampLimit) );
+
+               $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),
+                       $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );
+
+               wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
+
+               return true;
+       }
+}
+
+class MergeHistoryPager extends ReverseChronologicalPager {
+       public $mForm, $mConds;
+
+       function __construct( $form, $conds = array(), $source, $dest ) {
+               $this->mForm = $form;
+               $this->mConds = $conds;
+               $this->title = $source;
+               $this->articleID = $source->getArticleID();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $maxtimestamp = $dbr->selectField( 'revision', 'MIN(rev_timestamp)',
+                       array('rev_page' => $dest->getArticleID() ),
+                       __METHOD__ );
+               $this->maxTimestamp = $maxtimestamp;
+
+               parent::__construct();
+       }
+
+       function getStartBody() {
+               wfProfileIn( __METHOD__ );
+               # Do a link batch query
+               $this->mResult->seek( 0 );
+               $batch = new LinkBatch();
+               # Give some pointers to make (last) links
+               $this->mForm->prevId = array();
+               while( $row = $this->mResult->fetchObject() ) {
+                       $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );
+                       $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );
+
+                       $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;
+                       if( $rev_id > $row->rev_id )
+                               $this->mForm->prevId[$rev_id] = $row->rev_id;
+                       else if( $rev_id < $row->rev_id )
+                               $this->mForm->prevId[$row->rev_id] = $rev_id;
+
+                       $rev_id = $row->rev_id;
+               }
+
+               $batch->execute();
+               $this->mResult->seek( 0 );
+
+               wfProfileOut( __METHOD__ );
+               return '';
+       }
+
+       function formatRow( $row ) {
+               $block = new Block;
+               return $this->mForm->formatRevisionRow( $row );
+       }
+
+       function getQueryInfo() {
+               $conds = $this->mConds;
+               $conds['rev_page'] = $this->articleID;
+               $conds[] = "rev_timestamp < {$this->maxTimestamp}";
+
+               return array(
+                       'tables' => array('revision'),
+                       'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment',
+                                'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),
+                       'conds' => $conds
+               );
+       }
+
+       function getIndexField() {
+               return 'rev_timestamp';
+       }
+}
diff --git a/includes/specials/MissingFiles.php b/includes/specials/MissingFiles.php
new file mode 100644 (file)
index 0000000..d2da51c
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+/**
+ * A querypage to list the missing files - implements Special:Missingfiles
+ *
+ * @addtogroup SpecialPage
+ *
+ * @author MatÄ›j Grabovský <65s.mg@atlas.cz>
+ * @copyright Copyright Â© 2008, MatÄ›j Grabovský
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class MissingFilesPage extends QueryPage {
+       function getName() {
+               return 'Missingfiles';
+       }
+           
+       function isExpensive() {
+               return true;
+       }
+           
+       function isSyndicated() {
+               return false;
+       }
+       
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $imagelinks, $page ) = $dbr->tableNamesN( 'imagelinks', 'page' );
+               $name = $dbr->addQuotes( $this->getName() );
+               
+               return "SELECT $name as type,
+                        " . NS_IMAGE . " as namespace,
+                        il_to as title,
+                        COUNT(*) as value
+                        FROM $imagelinks
+                        LEFT JOIN $page ON il_to = page_title AND page_namespace = ". NS_IMAGE ."
+                        WHERE page_title IS NULL
+                        GROUP BY 1,2,3
+               ";
+       }
+       
+       function sortDescending() {
+               return true;
+       }
+       
+       /**
+        * Fetch user page links and cache their existence
+        */
+       function preprocessResults( $db, $res ) {
+               $batch = new LinkBatch;
+               
+               while ( $row = $db->fetchObject( $res ) )
+                       $batch->addObj( Title::makeTitleSafe( $row->namespace, $row->title ) );
+               
+               $batch->execute();
+               
+               // Back to start for display
+               if ( $db->numRows( $res ) > 0 )
+               
+               // If there are no rows we get an error seeking.
+               $db->dataSeek( $res, 0 );
+       }
+       
+       public function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+               
+               $nt = Title::makeTitle( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getText() );
+               
+               $plink = $this->isCached() 
+                       ? '<s>' . $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) . '</s>'
+                       : $skin->makeBrokenImageLinkObj( $nt, htmlspecialchars( $text ) );
+               
+               $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
+               $nlinks = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Whatlinkshere' ), $label, 'target=' . $nt->getPrefixedUrl() );
+               return wfSpecialList( $plink, $nlinks );
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialMissingFiles() {
+       list( $limit, $offset ) = wfCheckLimits();
+       
+       $wpp = new MissingFilesPage();
+       
+       $wpp->doQuery( $offset, $limit );
+}
\ No newline at end of file
diff --git a/includes/specials/Mostcategories.php b/includes/specials/Mostcategories.php
new file mode 100644 (file)
index 0000000..5df9c86
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * implements Special:Mostcategories
+ * @ingroup SpecialPage
+ */
+class MostcategoriesPage extends QueryPage {
+
+       function getName() { return 'Mostcategories'; }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $categorylinks, $page) = $dbr->tableNamesN( 'categorylinks', 'page' );
+               return
+                       "
+                       SELECT
+                               'Mostcategories' as type,
+                               page_namespace as namespace,
+                               page_title as title,
+                               COUNT(*) as value
+                       FROM $categorylinks
+                       LEFT JOIN $page ON cl_from = page_id
+                       WHERE page_namespace = " . NS_MAIN . "
+                       GROUP BY 1,2,3
+                       HAVING COUNT(*) > 1
+                       ";
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang;
+               $title = Title::makeTitleSafe( $result->namespace, $result->title );
+               if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); }
+               $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) );
+               $link = $skin->makeKnownLinkObj( $title, $title->getText() );
+               return wfSpecialList( $link, $count );
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostcategories() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new MostcategoriesPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Mostimages.php b/includes/specials/Mostimages.php
new file mode 100644 (file)
index 0000000..2fed0bd
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * implements Special:Mostimages
+ * @ingroup SpecialPage
+ */
+class MostimagesPage extends ImageQueryPage {
+
+       function getName() { return 'Mostimages'; }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $imagelinks = $dbr->tableName( 'imagelinks' );
+               return
+                       "
+                       SELECT
+                               'Mostimages' as type,
+                               " . NS_IMAGE . " as namespace,
+                               il_to as title,
+                               COUNT(*) as value
+                       FROM $imagelinks
+                       GROUP BY 1,2,3
+                       HAVING COUNT(*) > 1
+                       ";
+       }
+
+       function getCellHtml( $row ) {
+               global $wgLang;
+               return wfMsgExt( 'nlinks',  array( 'parsemag', 'escape' ),
+                       $wgLang->formatNum( $row->value ) ) . '<br />';
+       }
+
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialMostimages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new MostimagesPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Mostlinked.php b/includes/specials/Mostlinked.php
new file mode 100644 (file)
index 0000000..a56ac26
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page to show pages ordered by the number of pages linking to them.
+ * Implements Special:Mostlinked
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @copyright Â© 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class MostlinkedPage extends QueryPage {
+
+       function getName() { return 'Mostlinked'; }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       /**
+        * Note: Getting page_namespace only works if $this->isCached() is false
+        */
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $pagelinks, $page ) = $dbr->tableNamesN( 'pagelinks', 'page' );
+               return
+                       "SELECT 'Mostlinked' AS type,
+                               pl_namespace AS namespace,
+                               pl_title AS title,
+                               COUNT(*) AS value,
+                               page_namespace
+                       FROM $pagelinks
+                       LEFT JOIN $page ON pl_namespace=page_namespace AND pl_title=page_title
+                       GROUP BY 1,2,3,5
+                       HAVING COUNT(*) > 1";
+       }
+
+       /**
+        * Pre-fill the link cache
+        */
+       function preprocessResults( $db, $res ) {
+               if( $db->numRows( $res ) > 0 ) {
+                       $linkBatch = new LinkBatch();
+                       while( $row = $db->fetchObject( $res ) )
+                               $linkBatch->add( $row->namespace, $row->title );
+                       $db->dataSeek( $res, 0 );
+                       $linkBatch->execute();
+               }
+       }
+
+       /**
+        * Make a link to "what links here" for the specified title
+        *
+        * @param $title Title being queried
+        * @param $skin Skin to use
+        * @return string
+        */
+       function makeWlhLink( &$title, $caption, &$skin ) {
+               $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
+               return $skin->makeKnownLinkObj( $wlh, $caption );
+       }
+
+       /**
+        * Make links to the page corresponding to the item, and the "what links here" page for it
+        *
+        * @param $skin Skin to be used
+        * @param $result Result row
+        * @return string
+        */
+       function formatResult( $skin, $result ) {
+               global $wgLang;
+               $title = Title::makeTitleSafe( $result->namespace, $result->title );
+               $link = $skin->makeLinkObj( $title );
+               $wlh = $this->makeWlhLink( $title,
+                       wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
+                               $wgLang->formatNum( $result->value ) ), $skin );
+               return wfSpecialList( $link, $wlh );
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinked() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new MostlinkedPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Mostlinkedcategories.php b/includes/specials/Mostlinkedcategories.php
new file mode 100644 (file)
index 0000000..1b66d48
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to show categories ordered in descending order by the pages  in them
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class MostlinkedCategoriesPage extends QueryPage {
+
+       function getName() { return 'Mostlinkedcategories'; }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $categorylinks = $dbr->tableName( 'categorylinks' );
+               $name = $dbr->addQuotes( $this->getName() );
+               return
+                       "
+                       SELECT
+                               $name as type,
+                               " . NS_CATEGORY . " as namespace,
+                               cl_to as title,
+                               COUNT(*) as value
+                       FROM $categorylinks
+                       GROUP BY 1,2,3
+                       ";
+       }
+
+       function sortDescending() { return true; }
+
+       /**
+        * Fetch user page links and cache their existence
+        */
+       function preprocessResults( $db, $res ) {
+               $batch = new LinkBatch;
+               while ( $row = $db->fetchObject( $res ) )
+                       $batch->add( $row->namespace, $row->title );
+               $batch->execute();
+
+               // Back to start for display
+               if ( $db->numRows( $res ) > 0 )
+                       // If there are no rows we get an error seeking.
+                       $db->dataSeek( $res, 0 );
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+
+               $nt = Title::makeTitle( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getText() );
+
+               $plink = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) );
+
+               $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+                       $wgLang->formatNum( $result->value ) );
+               return wfSpecialList($plink, $nlinks);
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostlinkedCategories() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new MostlinkedCategoriesPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Mostlinkedtemplates.php b/includes/specials/Mostlinkedtemplates.php
new file mode 100644 (file)
index 0000000..b8d47e6
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+/**
+ * Special page lists templates with a large number of
+ * transclusion links, i.e. "most used" templates
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class SpecialMostlinkedtemplates extends QueryPage {
+
+       /**
+        * Name of the report
+        *
+        * @return string
+        */
+       public function getName() {
+               return 'Mostlinkedtemplates';
+       }
+
+       /**
+        * Is this report expensive, i.e should it be cached?
+        *
+        * @return bool
+        */
+       public function isExpensive() {
+               return true;
+       }
+
+       /**
+        * Is there a feed available?
+        *
+        * @return bool
+        */
+       public function isSyndicated() {
+               return false;
+       }
+
+       /**
+        * Sort the results in descending order?
+        *
+        * @return bool
+        */
+       public function sortDescending() {
+               return true;
+       }
+
+       /**
+        * Generate SQL for the report
+        *
+        * @return string
+        */
+       public function getSql() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $templatelinks = $dbr->tableName( 'templatelinks' );
+               $name = $dbr->addQuotes( $this->getName() );
+               return "SELECT {$name} AS type,
+                       " . NS_TEMPLATE . " AS namespace,
+                       tl_title AS title,
+                       COUNT(*) AS value
+                       FROM {$templatelinks}
+                       WHERE tl_namespace = " . NS_TEMPLATE . "
+                       GROUP BY 1, 2, 3";
+       }
+
+       /**
+        * Pre-cache page existence to speed up link generation
+        *
+        * @param Database $dbr Database connection
+        * @param int $res Result pointer
+        */
+       public function preprocessResults( $db, $res ) {
+               $batch = new LinkBatch();
+               while( $row = $db->fetchObject( $res ) ) {
+                       $batch->add( $row->namespace, $row->title );
+               }
+               $batch->execute();
+               if( $db->numRows( $res ) > 0 )
+                       $db->dataSeek( $res, 0 );
+       }
+
+       /**
+        * Format a result row
+        *
+        * @param Skin $skin Skin to use for UI elements
+        * @param object $result Result row
+        * @return string
+        */
+       public function formatResult( $skin, $result ) {
+               $title = Title::makeTitleSafe( $result->namespace, $result->title );
+               if( $title instanceof Title ) {
+                       return wfSpecialList(
+                               $skin->makeLinkObj( $title ),
+                               $this->makeWlhLink( $title, $skin, $result )
+                       );
+               } else {
+                       $tsafe = htmlspecialchars( $result->title );
+                       return "Invalid title in result set; {$tsafe}";
+               }
+       }
+
+       /**
+        * Make a "what links here" link for a given title
+        *
+        * @param Title $title Title to make the link for
+        * @param Skin $skin Skin to use
+        * @param object $result Result row
+        * @return string
+        */
+       private function makeWlhLink( $title, $skin, $result ) {
+               global $wgLang;
+               $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+               $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+                       $wgLang->formatNum( $result->value ) );
+               return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+       }
+}
+
+/**
+ * Execution function
+ *
+ * @param mixed $par Parameters passed to the page
+ */
+function wfSpecialMostlinkedtemplates( $par = false ) {
+       list( $limit, $offset ) = wfCheckLimits();
+       $mlt = new SpecialMostlinkedtemplates();
+       $mlt->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Mostrevisions.php b/includes/specials/Mostrevisions.php
new file mode 100644 (file)
index 0000000..001a08b
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * A special page to show pages in the
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class MostrevisionsPage extends QueryPage {
+
+       function getName() { return 'Mostrevisions'; }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $revision, $page ) = $dbr->tableNamesN( 'revision', 'page' );
+               return
+                       "
+                       SELECT
+                               'Mostrevisions' as type,
+                               page_namespace as namespace,
+                               page_title as title,
+                               COUNT(*) as value
+                       FROM $revision
+                       JOIN $page ON page_id = rev_page
+                       WHERE page_namespace = " . NS_MAIN . "
+                       GROUP BY 1,2,3
+                       HAVING COUNT(*) > 1
+                       ";
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+
+               $nt = Title::makeTitle( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+               $plink = $skin->makeKnownLinkObj( $nt, $text );
+
+               $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+                       $wgLang->formatNum( $result->value ) );
+               $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
+
+               return wfSpecialList($plink, $nlink);
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialMostrevisions() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new MostrevisionsPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Movepage.php b/includes/specials/Movepage.php
new file mode 100644 (file)
index 0000000..d08fb66
--- /dev/null
@@ -0,0 +1,428 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialMovepage( $par = null ) {
+       global $wgUser, $wgOut, $wgRequest, $action;
+
+       # Check for database lock
+       if ( wfReadOnly() ) {
+               $wgOut->readOnlyPage();
+               return;
+       }
+
+       $target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
+       $oldTitle = $wgRequest->getText( 'wpOldTitle', $target );
+       $newTitle = $wgRequest->getText( 'wpNewTitle' );
+
+       # Variables beginning with 'o' for old article 'n' for new article
+       $ot = Title::newFromText( $oldTitle );
+       $nt = Title::newFromText( $newTitle );
+
+       if( is_null( $ot ) ) {
+               $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+               return;
+       }
+       if( !$ot->exists() ) {
+               $wgOut->showErrorPage( 'nopagetitle', 'nopagetext' );
+               return;
+       }
+
+       # Check rights
+       $permErrors = $ot->getUserPermissionsErrors( 'move', $wgUser );
+       if( !empty( $permErrors ) ) {
+               $wgOut->showPermissionsErrorPage( $permErrors );
+               return;
+       }
+
+       $f = new MovePageForm( $ot, $nt );
+
+       if ( 'submit' == $action && $wgRequest->wasPosted()
+               && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+               $f->doSubmit();
+       } else {
+               $f->showForm( '' );
+       }
+}
+
+/**
+ * HTML form for Special:Movepage
+ * @ingroup SpecialPage
+ */
+class MovePageForm {
+       var $oldTitle, $newTitle, $reason; # Text input
+       var $moveTalk, $deleteAndMove, $moveSubpages;
+
+       private $watch = false;
+
+       function MovePageForm( $oldTitle, $newTitle ) {
+               global $wgRequest;
+               $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+               $this->oldTitle = $oldTitle;
+               $this->newTitle = $newTitle;
+               $this->reason = $wgRequest->getText( 'wpReason' );
+               if ( $wgRequest->wasPosted() ) {
+                       $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', false );
+               } else {
+                       $this->moveTalk = $wgRequest->getBool( 'wpMovetalk', true );
+               }
+               $this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false );
+               $this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
+               $this->watch = $wgRequest->getCheck( 'wpWatch' );
+       }
+
+       function showForm( $err, $hookErr = '' ) {
+               global $wgOut, $wgUser;
+
+               $ot = $this->oldTitle;
+               $sk = $wgUser->getSkin();
+
+               $oldTitleLink = $sk->makeLinkObj( $ot );
+               $oldTitle = $ot->getPrefixedText();
+
+               $wgOut->setPagetitle( wfMsg( 'move-page', $oldTitle ) );
+               $wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
+
+               if( $this->newTitle == '' ) {
+                       # Show the current title as a default
+                       # when the form is first opened.
+                       $newTitle = $oldTitle;
+               } else {
+                       if( $err == '' ) {
+                               $nt = Title::newFromURL( $this->newTitle );
+                               if( $nt ) {
+                                       # If a title was supplied, probably from the move log revert
+                                       # link, check for validity. We can then show some diagnostic
+                                       # information and save a click.
+                                       $newerr = $ot->isValidMoveOperation( $nt );
+                                       if( is_string( $newerr ) ) {
+                                               $err = $newerr;
+                                       }
+                               }
+                       }
+                       $newTitle = $this->newTitle;
+               }
+
+               if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) {
+                       $wgOut->addWikiMsg( 'delete_and_move_text', $newTitle );
+                       $movepagebtn = wfMsg( 'delete_and_move' );
+                       $submitVar = 'wpDeleteAndMove';
+                       $confirm = "
+                               <tr>
+                                       <td></td>
+                                       <td class='mw-input'>" .
+                                               Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) .
+                                       "</td>
+                               </tr>";
+                       $err = '';
+               } else {
+                       $wgOut->addWikiMsg( 'movepagetext' );
+                       $movepagebtn = wfMsg( 'movepagebtn' );
+                       $submitVar = 'wpMove';
+                       $confirm = false;
+               }
+
+               $oldTalk = $ot->getTalkPage();
+               $considerTalk = ( !$ot->isTalkPage() && $oldTalk->exists() );
+
+               if ( $considerTalk ) {
+                       $wgOut->addWikiMsg( 'movepagetalktext' );
+               }
+
+               $titleObj = SpecialPage::getTitleFor( 'Movepage' );
+               $token = htmlspecialchars( $wgUser->editToken() );
+
+               if ( $err != '' ) {
+                       $wgOut->setSubtitle( wfMsg( 'formerror' ) );
+                       $errMsg = "";
+                       if( $err == 'hookaborted' ) {
+                               $errMsg = "<p><strong class=\"error\">$hookErr</strong></p>\n";
+                       } else if (is_array($err)) {
+                               $errMsg = '<p><strong class="error">' . call_user_func_array( 'wfMsgWikiHtml', $err ) . "</strong></p>\n";
+                       } else {
+                               $errMsg = '<p><strong class="error">' . wfMsgWikiHtml( $err ) . "</strong></p>\n";
+                       }
+                       $wgOut->addHTML( $errMsg );
+               }
+
+               $wgOut->addHTML(
+                        Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
+                        Xml::openElement( 'fieldset' ) .
+                        Xml::element( 'legend', null, wfMsg( 'move-page-legend' ) ) .
+                        Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-movepage-table' ) ) .
+                        "<tr>
+                               <td class='mw-label'>" .
+                                       wfMsgHtml( 'movearticle' ) .
+                               "</td>
+                               <td class='mw-input'>
+                                       <strong>{$oldTitleLink}</strong>
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class='mw-label'>" .
+                                       Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
+                               "</td>
+                               <td class='mw-input'>" .
+                                       Xml::input( 'wpNewTitle', 40, $newTitle, array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
+                                       Xml::hidden( 'wpOldTitle', $oldTitle ) .
+                               "</td>
+                       </tr>
+                       <tr>
+                               <td class='mw-label'>" .
+                                       Xml::label( wfMsg( 'movereason' ), 'wpReason' ) .
+                               "</td>
+                               <td class='mw-input'>" .
+                                       Xml::tags( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2 ), htmlspecialchars( $this->reason ) ) .
+                               "</td>
+                       </tr>"
+               );
+
+               if( $considerTalk ) {
+                       $wgOut->addHTML( "
+                               <tr>
+                                       <td></td>
+                                       <td class='mw-input'>" .
+                                               Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $this->moveTalk ) .
+                                       "</td>
+                               </tr>"
+                       );
+               }
+
+               if( ($ot->hasSubpages() || $ot->getTalkPage()->hasSubpages())
+               && $ot->userCan( 'move-subpages' ) ) {
+                       $wgOut->addHTML( "
+                               <tr>
+                                       <td></td>
+                                       <td class=\"mw-input\">" .
+                               Xml::checkLabel( wfMsgHtml(
+                                               $ot->hasSubpages()
+                                               ? 'move-subpages'
+                                               : 'move-talk-subpages'
+                                       ),
+                                       'wpMovesubpages', 'wpMovesubpages',
+                                       # Don't check the box if we only have talk subpages to
+                                       # move and we aren't moving the talk page.
+                                       $this->moveSubpages && ($ot->hasSubpages() || $this->moveTalk)
+                               ) .
+                                       "</td>
+                               </tr>"
+                       );
+               }
+
+               $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) || $ot->userIsWatching();
+               $wgOut->addHTML( "
+                       <tr>
+                               <td></td>
+                               <td class='mw-input'>" .
+                                       Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
+                               "</td>
+                       </tr>
+                               {$confirm}
+                       <tr>
+                               <td>&nbsp;</td>
+                               <td class='mw-submit'>" .
+                                       Xml::submitButton( $movepagebtn, array( 'name' => $submitVar ) ) .
+                               "</td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::hidden( 'wpEditToken', $token ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) .
+                       "\n"
+               );
+
+               $this->showLogFragment( $ot, $wgOut );
+
+       }
+
+       function doSubmit() {
+               global $wgOut, $wgUser, $wgRequest, $wgMaximumMovedPages, $wgLang;
+
+               if ( $wgUser->pingLimiter( 'move' ) ) {
+                       $wgOut->rateLimited();
+                       return;
+               }
+
+               $ot = $this->oldTitle;
+               $nt = $this->newTitle;
+
+               # Delete to make way if requested
+               if ( $wgUser->isAllowed( 'delete' ) && $this->deleteAndMove ) {
+                       $article = new Article( $nt );
+
+                       # Disallow deletions of big articles
+                       $bigHistory = $article->isBigDeletion();
+                       if( $bigHistory && !$nt->userCan( 'bigdelete' ) ) {
+                               global $wgLang, $wgDeleteRevisionsLimit;
+                               $this->showForm( array('delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+                               return;
+                       }
+
+                       // This may output an error message and exit
+                       $article->doDelete( wfMsgForContent( 'delete_and_move_reason' ) );
+               }
+
+               # don't allow moving to pages with # in
+               if ( !$nt || $nt->getFragment() != '' ) {
+                       $this->showForm( 'badtitletext' );
+                       return;
+               }
+
+               $error = $ot->moveTo( $nt, true, $this->reason );
+               if ( $error !== true ) {
+                       # FIXME: showForm() should handle multiple errors
+                       call_user_func_array(array($this, 'showForm'), $error[0]);
+                       return;
+               }
+
+               wfRunHooks( 'SpecialMovepageAfterMove', array( &$this , &$ot , &$nt ) ) ;
+
+               $wgOut->setPagetitle( wfMsg( 'pagemovedsub' ) );
+
+               $oldUrl = $ot->getFullUrl( 'redirect=no' );
+               $newUrl = $nt->getFullUrl();
+               $oldText = $ot->getPrefixedText();
+               $newText = $nt->getPrefixedText();
+               $oldLink = "<span class='plainlinks'>[$oldUrl $oldText]</span>";
+               $newLink = "<span class='plainlinks'>[$newUrl $newText]</span>";
+
+               $wgOut->addWikiMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText );
+
+               # Now we move extra pages we've been asked to move: subpages and talk
+               # pages.  First, if the old page or the new page is a talk page, we
+               # can't move any talk pages: cancel that.
+               if( $ot->isTalkPage() || $nt->isTalkPage() ) {
+                       $this->moveTalk = false;
+               }
+
+               if( !$ot->userCan( 'move-subpages' ) ) {
+                       $this->moveSubpages = false;
+               }
+
+               # Next make a list of id's.  This might be marginally less efficient
+               # than a more direct method, but this is not a highly performance-cri-
+               # tical code path and readable code is more important here.
+               #
+               # Note: this query works nicely on MySQL 5, but the optimizer in MySQL
+               # 4 might get confused.  If so, consider rewriting as a UNION.
+               #
+               # If the target namespace doesn't allow subpages, moving with subpages
+               # would mean that you couldn't move them back in one operation, which
+               # is bad.  FIXME: A specific error message should be given in this
+               # case.
+               $dbr = wfGetDB( DB_MASTER );
+               if( $this->moveSubpages && (
+                       MWNamespace::hasSubpages( $nt->getNamespace() ) || (
+                               $this->moveTalk &&
+                               MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() )
+                       )
+               ) ) {
+                       $conds = array(
+                               'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' )
+                                       .' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
+                       );
+                       $conds['page_namespace'] = array();
+                       if( MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
+                               $conds['page_namespace'] []= $ot->getNamespace();
+                       }
+                       if( $this->moveTalk && MWNamespace::hasSubpages( $nt->getTalkPage()->getNamespace() ) ) {
+                               $conds['page_namespace'] []= $ot->getTalkPage()->getNamespace();
+                       }
+               } elseif( $this->moveTalk ) {
+                       $conds = array(
+                               'page_namespace' => $ot->getTalkPage()->getNamespace(),
+                               'page_title' => $ot->getDBKey()
+                       );
+               } else {
+                       # Skip the query
+                       $conds = null;
+               }
+
+               $extrapages = array();
+               if( !is_null( $conds ) ) {
+                       $extrapages = $dbr->select( 'page',
+                               array( 'page_id', 'page_namespace', 'page_title' ),
+                               $conds,
+                               __METHOD__
+                       );
+               }
+
+               $extraOutput = array();
+               $skin = $wgUser->getSkin();
+               $count = 1;
+               foreach( $extrapages as $row ) {
+                       if( $row->page_id == $ot->getArticleId() ) {
+                               # Already did this one.
+                               continue;
+                       }
+
+                       $oldPage = Title::newFromRow( $row );
+                       $newPageName = preg_replace(
+                               '#^'.preg_quote( $ot->getDBKey(), '#' ).'#',
+                               $nt->getDBKey(),
+                               $oldPage->getDBKey()
+                       );
+                       if( $oldPage->isTalkPage() ) {
+                               $newNs = $nt->getTalkPage()->getNamespace();
+                       } else {
+                               $newNs = $nt->getSubjectPage()->getNamespace();
+                       }
+                       # Bug 14385: we need makeTitleSafe because the new page names may
+                       # be longer than 255 characters.
+                       $newPage = Title::makeTitleSafe( $newNs, $newPageName );
+                       if( !$newPage ) {
+                               $oldLink = $skin->makeKnownLinkObj( $oldPage );
+                               $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
+                                       htmlspecialchars(Title::makeName( $newNs, $newPageName )));
+                               continue;
+                       }
+
+                       # This was copy-pasted from Renameuser, bleh.
+                       if ( $newPage->exists() && !$oldPage->isValidMoveTarget( $newPage ) ) {
+                               $link = $skin->makeKnownLinkObj( $newPage );
+                               $extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
+                       } else {
+                               $success = $oldPage->moveTo( $newPage, true, $this->reason );
+                               if( $success === true ) {
+                                       $oldLink = $skin->makeKnownLinkObj( $oldPage, '', 'redirect=no' );
+                                       $newLink = $skin->makeKnownLinkObj( $newPage );
+                                       $extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
+                               } else {
+                                       $oldLink = $skin->makeKnownLinkObj( $oldPage );
+                                       $newLink = $skin->makeLinkObj( $newPage );
+                                       $extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink, $newLink );
+                               }
+                       }
+
+                       ++$count;
+                       if( $count >= $wgMaximumMovedPages ) {
+                               $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) );
+                               break;
+                       }
+               }
+
+               if( $extraOutput !== array() ) {
+                       $wgOut->addHTML( "<ul>\n<li>" . implode( "</li>\n<li>", $extraOutput ) . "</li>\n</ul>" );
+               }
+
+               # Deal with watches (we don't watch subpages)
+               if( $this->watch ) {
+                       $wgUser->addWatch( $ot );
+                       $wgUser->addWatch( $nt );
+               } else {
+                       $wgUser->removeWatch( $ot );
+                       $wgUser->removeWatch( $nt );
+               }
+       }
+
+       function showLogFragment( $title, &$out ) {
+               $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'move' ) ) );
+               LogEventsList::showLogExtract( $out, 'move', $title->getPrefixedText() );
+       }
+
+}
diff --git a/includes/specials/Newimages.php b/includes/specials/Newimages.php
new file mode 100644 (file)
index 0000000..5fd37e8
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ * FIXME: this code is crap, should use Pager and Database::select().
+ */
+
+/**
+ *
+ */
+function wfSpecialNewimages( $par, $specialPage ) {
+       global $wgUser, $wgOut, $wgLang, $wgRequest, $wgGroupPermissions, $wgMiserMode;
+
+       $wpIlMatch = $wgRequest->getText( 'wpIlMatch' );
+       $dbr = wfGetDB( DB_SLAVE );
+       $sk = $wgUser->getSkin();
+       $shownav = !$specialPage->including();
+       $hidebots = $wgRequest->getBool('hidebots',1);
+
+       $hidebotsql = '';
+       if ($hidebots) {
+
+               /** Make a list of group names which have the 'bot' flag
+                   set.
+               */
+               $botconds=array();
+               foreach ($wgGroupPermissions as $groupname=>$perms) {
+                       if(array_key_exists('bot',$perms) && $perms['bot']) {
+                               $botconds[]="ug_group='$groupname'";
+                       }
+               }
+
+               /* If not bot groups, do not set $hidebotsql */
+               if ($botconds) {
+                       $isbotmember=$dbr->makeList($botconds, LIST_OR);
+
+                       /** This join, in conjunction with WHERE ug_group
+                           IS NULL, returns only those rows from IMAGE
+                       where the uploading user is not a member of
+                       a group which has the 'bot' permission set.
+                       */
+                       $ug = $dbr->tableName('user_groups');
+                       $hidebotsql = " LEFT OUTER JOIN $ug ON img_user=ug_user AND ($isbotmember)";
+               }
+       }
+
+       $image = $dbr->tableName('image');
+
+       $sql="SELECT img_timestamp from $image";
+       if ($hidebotsql) {
+               $sql .= "$hidebotsql WHERE ug_group IS NULL";
+       }
+       $sql.=' ORDER BY img_timestamp DESC LIMIT 1';
+       $res = $dbr->query($sql, 'wfSpecialNewImages');
+       $row = $dbr->fetchRow($res);
+       if($row!==false) {
+               $ts=$row[0];
+       } else {
+               $ts=false;
+       }
+       $dbr->freeResult($res);
+       $sql='';
+
+       /** If we were clever, we'd use this to cache. */
+       $latestTimestamp = wfTimestamp( TS_MW, $ts);
+
+       /** Hardcode this for now. */
+       $limit = 48;
+
+       if ( $parval = intval( $par ) ) {
+               if ( $parval <= $limit && $parval > 0 ) {
+                       $limit = $parval;
+               }
+       }
+
+       $where = array();
+       $searchpar = '';
+       if ( $wpIlMatch != '' && !$wgMiserMode) {
+               $nt = Title::newFromUrl( $wpIlMatch );
+               if($nt ) {
+                       $m = $dbr->strencode( strtolower( $nt->getDBkey() ) );
+                       $m = str_replace( '%', "\\%", $m );
+                       $m = str_replace( '_', "\\_", $m );
+                       $where[] = "LOWER(img_name) LIKE '%{$m}%'";
+                       $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
+               }
+       }
+
+       $invertSort = false;
+       if( $until = $wgRequest->getVal( 'until' ) ) {
+               $where[] = "img_timestamp < '" . $dbr->timestamp( $until ) . "'";
+       }
+       if( $from = $wgRequest->getVal( 'from' ) ) {
+               $where[] = "img_timestamp >= '" . $dbr->timestamp( $from ) . "'";
+               $invertSort = true;
+       }
+       $sql='SELECT img_size, img_name, img_user, img_user_text,'.
+            "img_description,img_timestamp FROM $image";
+
+       if($hidebotsql) {
+               $sql .= $hidebotsql;
+               $where[]='ug_group IS NULL';
+       }
+       if(count($where)) {
+               $sql.=' WHERE '.$dbr->makeList($where, LIST_AND);
+       }
+       $sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' );
+       $sql.=' LIMIT '.($limit+1);
+       $res = $dbr->query($sql, 'wfSpecialNewImages');
+
+       /**
+        * We have to flip things around to get the last N after a certain date
+        */
+       $images = array();
+       while ( $s = $dbr->fetchObject( $res ) ) {
+               if( $invertSort ) {
+                       array_unshift( $images, $s );
+               } else {
+                       array_push( $images, $s );
+               }
+       }
+       $dbr->freeResult( $res );
+
+       $gallery = new ImageGallery();
+       $firstTimestamp = null;
+       $lastTimestamp = null;
+       $shownImages = 0;
+       foreach( $images as $s ) {
+               if( ++$shownImages > $limit ) {
+                       # One extra just to test for whether to show a page link;
+                       # don't actually show it.
+                       break;
+               }
+
+               $name = $s->img_name;
+               $ut = $s->img_user_text;
+
+               $nt = Title::newFromText( $name, NS_IMAGE );
+               $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
+
+               $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
+
+               $timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
+               if( empty( $firstTimestamp ) ) {
+                       $firstTimestamp = $timestamp;
+               }
+               $lastTimestamp = $timestamp;
+       }
+
+       $bydate = wfMsg( 'bydate' );
+       $lt = $wgLang->formatNum( min( $shownImages, $limit ) );
+       if ($shownav) {
+               $text = wfMsgExt( 'imagelisttext', array('parse'), $lt, $bydate );
+               $wgOut->addHTML( $text . "\n" );
+       }
+
+       $sub = wfMsg( 'ilsubmit' );
+       $titleObj = SpecialPage::getTitleFor( 'Newimages' );
+       $action = $titleObj->escapeLocalURL( $hidebots ? '' : 'hidebots=0' );
+       if ($shownav && !$wgMiserMode) {
+               $wgOut->addHTML( "<form id=\"imagesearch\" method=\"post\" action=\"" .
+                 "{$action}\">" .
+                       Xml::input( 'wpIlMatch', 20, $wpIlMatch ) . ' ' .
+                 Xml::submitButton( $sub, array( 'name' => 'wpIlSubmit' ) ) .
+                 "</form>" );
+       }
+
+       /**
+        * Paging controls...
+        */
+
+       # If we change bot visibility, this needs to be carried along.
+       if(!$hidebots) {
+               $botpar='&hidebots=0';
+       } else {
+               $botpar='';
+       }
+       $now = wfTimestampNow();
+       $d = $wgLang->date( $now, true );
+       $t = $wgLang->time( $now, true );
+       $dateLink = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'sp-newimages-showfrom', $d, $t ), 
+               'from='.$now.$botpar.$searchpar );
+
+       $botLink = $sk->makeKnownLinkObj($titleObj, wfMsgHtml( 'showhidebots', 
+               ($hidebots ? wfMsgHtml('show') : wfMsgHtml('hide'))),'hidebots='.($hidebots ? '0' : '1').$searchpar);
+
+       $prevLink = wfMsgHtml( 'prevn', $wgLang->formatNum( $limit ) );
+       if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
+               $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
+       }
+
+       $nextLink = wfMsgHtml( 'nextn', $wgLang->formatNum( $limit ) );
+       if( $shownImages > $limit && $lastTimestamp ) {
+               $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
+       }
+
+       $prevnext = '<p>' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>';
+
+       if ($shownav)
+               $wgOut->addHTML( $prevnext );
+
+       if( count( $images ) ) {
+               $wgOut->addHTML( $gallery->toHTML() );
+               if ($shownav)
+                       $wgOut->addHTML( $prevnext );
+       } else {
+               $wgOut->addWikiMsg( 'noimages' );
+       }
+}
diff --git a/includes/specials/Newpages.php b/includes/specials/Newpages.php
new file mode 100644 (file)
index 0000000..06252a8
--- /dev/null
@@ -0,0 +1,445 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+
+/**
+ * Start point
+ */
+function wfSpecialNewPages( $par, $sp ) {
+       $page = new NewPagesForm();
+       $page->execute( $par, $sp->including() );
+}
+
+/**
+ * implements Special:Newpages
+ * @ingroup SpecialPage
+ */
+class NewPagesForm {
+
+       // Stored objects
+       protected $opts, $title, $skin;
+
+       // Some internal settings
+       protected $showNavigation = false;
+
+       protected function setup( $par ) {
+               global $wgRequest, $wgUser, $wgEnableNewpagesUserFilter;
+
+               // Options
+               $opts = new FormOptions();
+               $this->opts = $opts; // bind
+               $opts->add( 'hideliu', false );
+               $opts->add( 'hidepatrolled', false );
+               $opts->add( 'hidebots', false );
+               $opts->add( 'limit', 50 );
+               $opts->add( 'offset', '' );
+               $opts->add( 'namespace', '0' );
+               $opts->add( 'username', '' );
+               $opts->add( 'feed', '' );
+
+               // Set values
+               $opts->fetchValuesFromRequest( $wgRequest );
+               if ( $par ) $this->parseParams( $par );
+
+               // Validate
+               $opts->validateIntBounds( 'limit', 0, 5000 );
+               if( !$wgEnableNewpagesUserFilter ) {
+                       $opts->setValue( 'username', '' );
+               }
+
+               // Store some objects
+               $this->skin = $wgUser->getSkin();
+               $this->title = SpecialPage::getTitleFor( 'NewPages' );
+       }
+
+       protected function parseParams( $par ) {
+               global $wgLang;
+               $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+               foreach ( $bits as $bit ) {
+                       if ( 'shownav' == $bit )
+                               $this->showNavigation = true;
+                       if ( 'hideliu' === $bit )
+                               $this->opts->setValue( 'hideliu', true );
+                       if ( 'hidepatrolled' == $bit )
+                               $this->opts->setValue( 'hidepatrolled', true );
+                       if ( 'hidebots' == $bit )
+                               $this->opts->setValue( 'hidebots', true );
+                       if ( is_numeric( $bit ) )
+                               $this->opts->setValue( 'limit', intval( $bit ) );
+
+                       $m = array();
+                       if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) )
+                               $this->opts->setValue( 'limit', intval($m[1]) );
+                       // PG offsets not just digits!
+                       if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) )
+                               $this->opts->setValue( 'offset',  intval($m[1]) );
+                       if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
+                               $ns = $wgLang->getNsIndex( $m[1] );
+                               if( $ns !== false ) {
+                                       $this->opts->setValue( 'namespace',  $ns );
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Show a form for filtering namespace and username
+        *
+        * @param string $par
+        * @param bool $including true if the page is being included with {{Special:Newpages}}
+        * @return string
+        */
+       public function execute( $par, $including ) {
+               global $wgLang, $wgGroupPermissions, $wgUser, $wgOut;
+
+               $this->showNavigation = !$including; // Maybe changed in setup
+               $this->setup( $par );
+
+               if( !$including ) {
+                       // Settings
+                       $this->form();
+
+                       $this->setSyndicated();
+                       $feedType = $this->opts->getValue( 'feed' );
+                       if( $feedType ) {
+                               return $this->feed( $feedType );
+                       }
+               }
+
+               $pager = new NewPagesPager( $this, $this->opts );
+               $pager->mLimit = $this->opts->getValue( 'limit' );
+               $pager->mOffset = $this->opts->getValue( 'offset' );
+
+               if( $pager->getNumRows() ) {
+                       $navigation = '';
+                       if ( $this->showNavigation ) $navigation = $pager->getNavigationBar();
+                       $wgOut->addHTML( $navigation . $pager->getBody() . $navigation );
+               } else {
+                       $wgOut->addWikiMsg( 'specialpage-empty' );
+               }
+       }
+
+       protected function filterLinks() {
+               global $wgGroupPermissions, $wgUser;
+
+               // show/hide links
+               $showhide = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
+
+               // Option value -> message mapping
+               $filters = array(
+                       'hideliu' => 'rcshowhideliu',
+                       'hidepatrolled' => 'rcshowhidepatr',
+                       'hidebots' => 'rcshowhidebots'
+               );
+
+               // Disable some if needed
+               if ( $wgGroupPermissions['*']['createpage'] !== true )
+                       unset($filters['hideliu']);
+
+               if ( !$wgUser->useNPPatrol() )
+                       unset($filters['hidepatrolled']);
+
+               $links = array();
+               $changed = $this->opts->getChangedValues();
+               unset($changed['offset']); // Reset offset if query type changes
+
+               foreach ( $filters as $key => $msg ) {
+                       $onoff = 1 - $this->opts->getValue($key);
+                       $link = $this->skin->makeKnownLinkObj( $this->title, $showhide[$onoff],
+                               wfArrayToCGI( array( $key => $onoff ), $changed )
+                       );
+                       $links[$key] = wfMsgHtml( $msg, $link );
+               }
+
+               return implode( ' | ', $links );
+       }
+
+       protected function form() {
+               global $wgOut, $wgEnableNewpagesUserFilter, $wgScript;
+
+               // Consume values
+               $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
+               $namespace = $this->opts->consumeValue( 'namespace' );
+               $username = $this->opts->consumeValue( 'username' );
+
+               // Check username input validity
+               $ut = Title::makeTitleSafe( NS_USER, $username );
+               $userText = $ut ? $ut->getText() : '';
+
+               // Store query values in hidden fields so that form submission doesn't lose them
+               $hidden = array();
+               foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
+                       $hidden[] = Xml::hidden( $key, $value );
+               }
+               $hidden = implode( "\n", $hidden );
+
+               $form = Xml::openElement( 'form', array( 'action' => $wgScript ) ) .
+                       Xml::hidden( 'title', $this->title->getPrefixedDBkey() ) .
+                       Xml::fieldset( wfMsg( 'newpages' ) ) .
+                       Xml::openElement( 'table', array( 'id' => 'mw-newpages-table' ) ) .
+                       "<tr>
+                               <td class='mw-label'>" .
+                                       Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+                               "</td>
+                               <td class='mw-input'>" .
+                                       Xml::namespaceSelector( $namespace, 'all' ) .
+                               "</td>
+                       </tr>" .
+                       ($wgEnableNewpagesUserFilter ?
+                       "<tr>
+                               <td class='mw-label'>" .
+                                       Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) .
+                               "</td>
+                               <td class='mw-input'>" .
+                                       Xml::input( 'username', 30, $userText, array( 'id' => 'mw-np-username' ) ) .
+                               "</td>
+                       </tr>" : "" ) .
+                       "<tr> <td></td>
+                               <td class='mw-submit'>" .
+                                       Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+                               "</td>
+                       </tr>" .
+                       "<tr>
+                               <td></td>
+                               <td class='mw-input'>" .
+                                       $this->filterLinks() .
+                               "</td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       $hidden .
+                       Xml::closeElement( 'form' );
+
+               $wgOut->addHTML( $form );
+       }
+
+       protected function setSyndicated() {
+               global $wgOut;
+               $queryParams = array(
+                       'namespace' => $this->opts->getValue( 'namespace' ),
+                       'username' => $this->opts->getValue( 'username' )
+               );
+               $wgOut->setSyndicated( true );
+               $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
+       }
+
+       /**
+        * Format a row, providing the timestamp, links to the page/history, size, user links, and a comment
+        *
+        * @param $skin Skin to use
+        * @param $result Result row
+        * @return string
+        */
+       public function formatRow( $result ) {
+               global $wgLang, $wgContLang, $wgUser;
+               $dm = $wgContLang->getDirMark();
+
+               $title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title );
+               $time = $wgLang->timeAndDate( $result->rc_timestamp, true );
+               $plink = $this->skin->makeKnownLinkObj( $title, '', $this->patrollable( $result ) ? 'rcid=' . $result->rc_id : '' );
+               $hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+               $length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
+                       $wgLang->formatNum( $result->length ) );
+               $ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' .
+                       $this->skin->userToolLinks( $result->rc_user, $result->rc_user_text );
+               $comment = $this->skin->commentBlock( $result->rc_comment );
+               $css = $this->patrollable( $result ) ? " class='not-patrolled'" : '';
+
+               return "<li{$css}>{$time} {$dm}{$plink} ({$hist}) {$dm}[{$length}] {$dm}{$ulink} {$comment}</li>\n";
+       }
+
+       /**
+        * Should a specific result row provide "patrollable" links?
+        *
+        * @param $result Result row
+        * @return bool
+        */
+       protected function patrollable( $result ) {
+               global $wgUser;
+               return ( $wgUser->useNPPatrol() && !$result->rc_patrolled );
+       }
+
+       /**
+        * Output a subscription feed listing recent edits to this page.
+        * @param string $type
+        */
+       protected function feed( $type ) {
+               require_once 'SpecialRecentchanges.php';
+
+               global $wgFeed, $wgFeedClasses;
+
+               if ( !$wgFeed ) {
+                       global $wgOut;
+                       $wgOut->addWikiMsg( 'feed-unavailable' );
+                       return;
+               }
+
+               if( !isset( $wgFeedClasses[$type] ) ) {
+                       global $wgOut;
+                       $wgOut->addWikiMsg( 'feed-invalid' );
+                       return;
+               }
+
+               $feed = new $wgFeedClasses[$type](
+                       $this->feedTitle(),
+                       wfMsg( 'tagline' ),
+                       $this->title->getFullUrl() );
+
+               $pager = new NewPagesPager( $this, $this->opts );
+               $limit = $this->opts->getValue( 'limit' );
+               global $wgFeedLimit;
+               if( $limit > $wgFeedLimit ) {
+                       $limit = $wgFeedLimit;
+               }
+               $pager->mLimit = $limit;
+
+               $feed->outHeader();
+               if( $pager->getNumRows() > 0 ) {
+                       while( $row = $pager->mResult->fetchObject() ) {
+                               $feed->outItem( $this->feedItem( $row ) );
+                       }
+               }
+               $feed->outFooter();
+       }
+
+       protected function feedTitle() {
+               global $wgContLanguageCode, $wgSitename;
+               $page = SpecialPage::getPage( 'Newpages' );
+               $desc = $page->getDescription();
+               return "$wgSitename - $desc [$wgContLanguageCode]";
+       }
+
+       protected function feedItem( $row ) {
+               $title = Title::MakeTitle( intval( $row->rc_namespace ), $row->rc_title );
+               if( $title ) {
+                       $date = $row->rc_timestamp;
+                       $comments = $title->getTalkPage()->getFullURL();
+
+                       return new FeedItem(
+                               $title->getPrefixedText(),
+                               $this->feedItemDesc( $row ),
+                               $title->getFullURL(),
+                               $date,
+                               $this->feedItemAuthor( $row ),
+                               $comments);
+               } else {
+                       return NULL;
+               }
+       }
+
+       /**
+        * Quickie hack... strip out wikilinks to more legible form from the comment.
+        */
+       protected function stripComment( $text ) {
+               return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
+       }
+
+       protected function feedItemAuthor( $row ) {
+               return isset( $row->rc_user_text ) ? $row->rc_user_text : '';
+       }
+
+       protected function feedItemDesc( $row ) {
+               $revision = Revision::newFromId( $row->rev_id );
+               if( $revision ) {
+                       return '<p>' . htmlspecialchars( $revision->getUserText() ) . ': ' .
+                               htmlspecialchars( $revision->getComment() ) . 
+                               "</p>\n<hr />\n<div>" .
+                               nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+               }
+               return '';
+       }
+}
+
+/**
+ * @ingroup SpecialPage Pager
+ */
+class NewPagesPager extends ReverseChronologicalPager {
+       // Stored opts
+       protected $opts, $mForm;
+
+       private $hideliu, $hidepatrolled, $hidebots, $namespace, $user, $spTitle;
+
+       function __construct( $form, FormOptions $opts ) {
+               parent::__construct();
+               $this->mForm = $form;
+               $this->opts = $opts;
+       }
+
+       function getTitle(){
+               static $title = null;
+               if ( $title === null )
+                       $title = SpecialPage::getTitleFor( 'Newpages' );
+               return $title;
+       }
+
+       function getQueryInfo() {
+               global $wgEnableNewpagesUserFilter, $wgGroupPermissions, $wgUser;
+               $conds = array();
+               $conds['rc_new'] = 1;
+
+               $namespace = $this->opts->getValue( 'namespace' );
+               $namespace = ( $namespace === 'all' ) ? false : intval( $namespace );
+
+               $username = $this->opts->getValue( 'username' );
+               $user = Title::makeTitleSafe( NS_USER, $username );
+
+               if( $namespace !== false ) {
+                       $conds['rc_namespace'] = $namespace;
+                       $rcIndexes = array( 'new_name_timestamp' );
+               } else {
+                       $rcIndexes = array( 'rc_timestamp' );
+               }
+               $conds[] = 'page_id = rc_cur_id';
+               $conds['page_is_redirect'] = 0;
+               # $wgEnableNewpagesUserFilter - temp WMF hack
+               if( $wgEnableNewpagesUserFilter && $user ) {
+                       $conds['rc_user_text'] = $user->getText();
+                       $rcIndexes = 'rc_user_text';
+               # If anons cannot make new pages, don't "exclude logged in users"!
+               } elseif( $wgGroupPermissions['*']['createpage'] && $this->opts->getValue( 'hideliu' ) ) {
+                       $conds['rc_user'] = 0;
+               }
+               # If this user cannot see patrolled edits or they are off, don't do dumb queries!
+               if( $this->opts->getValue( 'hidepatrolled' ) && $wgUser->useNPPatrol() ) {
+                       $conds['rc_patrolled'] = 0;
+               }
+               if( $this->opts->getValue( 'hidebots' ) ) {
+                       $conds['rc_bot'] = 0;
+               }
+
+               return array(
+                       'tables' => array( 'recentchanges', 'page' ),
+                       'fields' => 'rc_namespace,rc_title, rc_cur_id, rc_user,rc_user_text,rc_comment,
+                               rc_timestamp,rc_patrolled,rc_id,page_len as length, page_latest as rev_id',
+                       'conds' => $conds,
+                       'options' => array( 'USE INDEX' => array('recentchanges' => $rcIndexes) )
+               );
+       }
+
+       function getIndexField() {
+               return 'rc_timestamp';
+       }
+
+       function formatRow( $row ) {
+               return $this->mForm->formatRow( $row );
+       }
+
+       function getStartBody() {
+               # Do a batch existence check on pages
+               $linkBatch = new LinkBatch();
+               while( $row = $this->mResult->fetchObject() ) {
+                       $linkBatch->add( NS_USER, $row->rc_user_text );
+                       $linkBatch->add( NS_USER_TALK, $row->rc_user_text );
+                       $linkBatch->add( $row->rc_namespace, $row->rc_title );
+               }
+               $linkBatch->execute();
+               return "<ul>";
+       }
+
+       function getEndBody() {
+               return "</ul>";
+       }
+}
diff --git a/includes/specials/Popularpages.php b/includes/specials/Popularpages.php
new file mode 100644 (file)
index 0000000..eb57273
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Popularpages
+ * @ingroup SpecialPage
+ */
+class PopularPagesPage extends QueryPage {
+
+       function getName() {
+               return "Popularpages";
+       }
+
+       function isExpensive() {
+               # page_counter is not indexed
+               return true;
+       }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $page = $dbr->tableName( 'page' );
+
+               $query =
+                       "SELECT 'Popularpages' as type,
+                               page_namespace as namespace,
+                               page_title as title,
+                               page_counter as value
+                       FROM $page ";
+               $where =
+                       "WHERE page_is_redirect=0 AND page_namespace";
+
+               global $wgContentNamespaces;
+               if( empty( $wgContentNamespaces ) ) {
+                       $where .= '='.NS_MAIN;
+               } else if( count( $wgContentNamespaces ) > 1 ) {
+                       $where .= ' in (' . implode( ', ', $wgContentNamespaces ) . ')';
+               } else {
+                       $where .= '='.$wgContentNamespaces[0];
+               }
+
+               return $query . $where;
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+               $title = Title::makeTitle( $result->namespace, $result->title );
+               $link = $skin->makeKnownLinkObj( $title, htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) ) );
+               $nv = wfMsgExt( 'nviews', array( 'parsemag', 'escape'),
+                       $wgLang->formatNum( $result->value ) );
+               return wfSpecialList($link, $nv);
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialPopularpages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $ppp = new PopularPagesPage();
+
+       return $ppp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Preferences.php b/includes/specials/Preferences.php
new file mode 100644 (file)
index 0000000..cdff571
--- /dev/null
@@ -0,0 +1,1125 @@
+<?php
+/**
+ * Hold things related to displaying and saving user preferences.
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point that create the "Preferences" object
+ */
+function wfSpecialPreferences() {
+       global $wgRequest;
+
+       $form = new PreferencesForm( $wgRequest );
+       $form->execute();
+}
+
+/**
+ * Preferences form handling
+ * This object will show the preferences form and can save it as well.
+ * @ingroup SpecialPage
+ */
+class PreferencesForm {
+       var $mQuickbar, $mOldpass, $mNewpass, $mRetypePass, $mStubs;
+       var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
+       var $mUserLanguage, $mUserVariant;
+       var $mSearch, $mRecent, $mRecentDays, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
+       var $mReset, $mPosted, $mToggles, $mUseAjaxSearch, $mSearchNs, $mRealName, $mImageSize;
+       var $mUnderline, $mWatchlistEdits;
+
+       /**
+        * Constructor
+        * Load some values
+        */
+       function PreferencesForm( &$request ) {
+               global $wgContLang, $wgUser, $wgAllowRealName;
+
+               $this->mQuickbar = $request->getVal( 'wpQuickbar' );
+               $this->mOldpass = $request->getVal( 'wpOldpass' );
+               $this->mNewpass = $request->getVal( 'wpNewpass' );
+               $this->mRetypePass =$request->getVal( 'wpRetypePass' );
+               $this->mStubs = $request->getVal( 'wpStubs' );
+               $this->mRows = $request->getVal( 'wpRows' );
+               $this->mCols = $request->getVal( 'wpCols' );
+               $this->mSkin = $request->getVal( 'wpSkin' );
+               $this->mMath = $request->getVal( 'wpMath' );
+               $this->mDate = $request->getVal( 'wpDate' );
+               $this->mUserEmail = $request->getVal( 'wpUserEmail' );
+               $this->mRealName = $wgAllowRealName ? $request->getVal( 'wpRealName' ) : '';
+               $this->mEmailFlag = $request->getCheck( 'wpEmailFlag' ) ? 0 : 1;
+               $this->mNick = $request->getVal( 'wpNick' );
+               $this->mUserLanguage = $request->getVal( 'wpUserLanguage' );
+               $this->mUserVariant = $request->getVal( 'wpUserVariant' );
+               $this->mSearch = $request->getVal( 'wpSearch' );
+               $this->mRecent = $request->getVal( 'wpRecent' );
+               $this->mRecentDays = $request->getVal( 'wpRecentDays' );
+               $this->mHourDiff = $request->getVal( 'wpHourDiff' );
+               $this->mSearchLines = $request->getVal( 'wpSearchLines' );
+               $this->mSearchChars = $request->getVal( 'wpSearchChars' );
+               $this->mImageSize = $request->getVal( 'wpImageSize' );
+               $this->mThumbSize = $request->getInt( 'wpThumbSize' );
+               $this->mUnderline = $request->getInt( 'wpOpunderline' );
+               $this->mAction = $request->getVal( 'action' );
+               $this->mReset = $request->getCheck( 'wpReset' );
+               $this->mPosted = $request->wasPosted();
+               $this->mSuccess = $request->getCheck( 'success' );
+               $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
+               $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
+               $this->mUseAjaxSearch = $request->getCheck( 'wpUseAjaxSearch' );
+               $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
+
+               $this->mSaveprefs = $request->getCheck( 'wpSaveprefs' ) &&
+                       $this->mPosted &&
+                       $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+
+               # User toggles  (the big ugly unsorted list of checkboxes)
+               $this->mToggles = array();
+               if ( $this->mPosted ) {
+                       $togs = User::getToggles();
+                       foreach ( $togs as $tname ) {
+                               $this->mToggles[$tname] = $request->getCheck( "wpOp$tname" ) ? 1 : 0;
+                       }
+               }
+
+               $this->mUsedToggles = array();
+
+               # Search namespace options
+               # Note: namespaces don't necessarily have consecutive keys
+               $this->mSearchNs = array();
+               if ( $this->mPosted ) {
+                       $namespaces = $wgContLang->getNamespaces();
+                       foreach ( $namespaces as $i => $namespace ) {
+                               if ( $i >= 0 ) {
+                                       $this->mSearchNs[$i] = $request->getCheck( "wpNs$i" ) ? 1 : 0;
+                               }
+                       }
+               }
+
+               # Validate language
+               if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) {
+                       $this->mUserLanguage = 'nolanguage';
+               }
+
+               wfRunHooks( 'InitPreferencesForm', array( $this, $request ) );
+       }
+
+       function execute() {
+               global $wgUser, $wgOut;
+
+               if ( $wgUser->isAnon() ) {
+                       $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext' );
+                       return;
+               }
+               if ( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+               if ( $this->mReset ) {
+                       $this->resetPrefs();
+                       $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) );
+               } else if ( $this->mSaveprefs ) {
+                       $this->savePreferences();
+               } else {
+                       $this->resetPrefs();
+                       $this->mainPrefsForm( '' );
+               }
+       }
+       /**
+        * @access private
+        */
+       function validateInt( &$val, $min=0, $max=0x7fffffff ) {
+               $val = intval($val);
+               $val = min($val, $max);
+               $val = max($val, $min);
+               return $val;
+       }
+
+       /**
+        * @access private
+        */
+       function validateFloat( &$val, $min, $max=0x7fffffff ) {
+               $val = floatval( $val );
+               $val = min( $val, $max );
+               $val = max( $val, $min );
+               return( $val );
+       }
+
+       /**
+        * @access private
+        */
+       function validateIntOrNull( &$val, $min=0, $max=0x7fffffff ) {
+               $val = trim($val);
+               if($val === '') {
+                       return null;
+               } else {
+                       return $this->validateInt( $val, $min, $max );
+               }
+       }
+
+       /**
+        * @access private
+        */
+       function validateDate( $val ) {
+               global $wgLang, $wgContLang;
+               if ( $val !== false && (
+                       in_array( $val, (array)$wgLang->getDatePreferences() ) ||
+                       in_array( $val, (array)$wgContLang->getDatePreferences() ) ) )
+               {
+                       return $val;
+               } else {
+                       return $wgLang->getDefaultDateFormat();
+               }
+       }
+
+       /**
+        * Used to validate the user inputed timezone before saving it as
+        * 'timecorrection', will return '00:00' if fed bogus data.
+        * Note: It's not a 100% correct implementation timezone-wise, it will
+        * accept stuff like '14:30',
+        * @access private
+        * @param string $s the user input
+        * @return string
+        */
+       function validateTimeZone( $s ) {
+               if ( $s !== '' ) {
+                       if ( strpos( $s, ':' ) ) {
+                               # HH:MM
+                               $array = explode( ':' , $s );
+                               $hour = intval( $array[0] );
+                               $minute = intval( $array[1] );
+                       } else {
+                               $minute = intval( $s * 60 );
+                               $hour = intval( $minute / 60 );
+                               $minute = abs( $minute ) % 60;
+                       }
+                       # Max is +14:00 and min is -12:00, see:
+                       # http://en.wikipedia.org/wiki/Timezone
+                       $hour = min( $hour, 14 );
+                       $hour = max( $hour, -12 );
+                       $minute = min( $minute, 59 );
+                       $minute = max( $minute, 0 );
+                       $s = sprintf( "%02d:%02d", $hour, $minute );
+               }
+               return $s;
+       }
+
+       /**
+        * @access private
+        */
+       function savePreferences() {
+               global $wgUser, $wgOut, $wgParser;
+               global $wgEnableUserEmail, $wgEnableEmail;
+               global $wgEmailAuthentication, $wgRCMaxAge;
+               global $wgAuth, $wgEmailConfirmToEdit;
+
+
+               if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) {
+                       if ( $this->mNewpass != $this->mRetypePass ) {
+                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'badretype' ) );
+                               $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) );
+                               return;
+                       }
+
+                       if (!$wgUser->checkPassword( $this->mOldpass )) {
+                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'wrongpassword' ) );
+                               $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) );
+                               return;
+                       }
+
+                       try {
+                               $wgUser->setPassword( $this->mNewpass );
+                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'success' ) );
+                               $this->mNewpass = $this->mOldpass = $this->mRetypePass = '';
+                       } catch( PasswordError $e ) {
+                               wfRunHooks( 'PrefsPasswordAudit', array( $wgUser, $this->mNewpass, 'error' ) );
+                               $this->mainPrefsForm( 'error', $e->getMessage() );
+                               return;
+                       }
+               }
+               $wgUser->setRealName( $this->mRealName );
+               $oldOptions = $wgUser->mOptions;
+
+               if( $wgUser->getOption( 'language' ) !== $this->mUserLanguage ) {
+                       $needRedirect = true;
+               } else {
+                       $needRedirect = false;
+               }
+
+               # Validate the signature and clean it up as needed
+               global $wgMaxSigChars;
+               if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
+                       global $wgLang;
+                       $this->mainPrefsForm( 'error',
+                               wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) );
+                       return;
+               } elseif( $this->mToggles['fancysig'] ) {
+                       if( $wgParser->validateSig( $this->mNick ) !== false ) {
+                               $this->mNick = $wgParser->cleanSig( $this->mNick );
+                       } else {
+                               $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) );
+                               return;
+                       }
+               } else {
+                       // When no fancy sig used, make sure ~{3,5} get removed.
+                       $this->mNick = $wgParser->cleanSigInSig( $this->mNick );
+               }
+
+               $wgUser->setOption( 'language', $this->mUserLanguage );
+               $wgUser->setOption( 'variant', $this->mUserVariant );
+               $wgUser->setOption( 'nickname', $this->mNick );
+               $wgUser->setOption( 'quickbar', $this->mQuickbar );
+               $wgUser->setOption( 'skin', $this->mSkin );
+               global $wgUseTeX;
+               if( $wgUseTeX ) {
+                       $wgUser->setOption( 'math', $this->mMath );
+               }
+               $wgUser->setOption( 'date', $this->validateDate( $this->mDate ) );
+               $wgUser->setOption( 'searchlimit', $this->validateIntOrNull( $this->mSearch ) );
+               $wgUser->setOption( 'contextlines', $this->validateIntOrNull( $this->mSearchLines ) );
+               $wgUser->setOption( 'contextchars', $this->validateIntOrNull( $this->mSearchChars ) );
+               $wgUser->setOption( 'rclimit', $this->validateIntOrNull( $this->mRecent ) );
+               $wgUser->setOption( 'rcdays', $this->validateInt($this->mRecentDays, 1, ceil($wgRCMaxAge / (3600*24))));
+               $wgUser->setOption( 'wllimit', $this->validateIntOrNull( $this->mWatchlistEdits, 0, 1000 ) );
+               $wgUser->setOption( 'rows', $this->validateInt( $this->mRows, 4, 1000 ) );
+               $wgUser->setOption( 'cols', $this->validateInt( $this->mCols, 4, 1000 ) );
+               $wgUser->setOption( 'stubthreshold', $this->validateIntOrNull( $this->mStubs ) );
+               $wgUser->setOption( 'timecorrection', $this->validateTimeZone( $this->mHourDiff, -12, 14 ) );
+               $wgUser->setOption( 'imagesize', $this->mImageSize );
+               $wgUser->setOption( 'thumbsize', $this->mThumbSize );
+               $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
+               $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
+               $wgUser->setOption( 'ajaxsearch', $this->mUseAjaxSearch );
+               $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
+
+               # Set search namespace options
+               foreach( $this->mSearchNs as $i => $value ) {
+                       $wgUser->setOption( "searchNs{$i}", $value );
+               }
+
+               if( $wgEnableEmail && $wgEnableUserEmail ) {
+                       $wgUser->setOption( 'disablemail', $this->mEmailFlag );
+               }
+
+               # Set user toggles
+               foreach ( $this->mToggles as $tname => $tvalue ) {
+                       $wgUser->setOption( $tname, $tvalue );
+               }
+
+               $error = false;
+               if( $wgEnableEmail ) {
+                       $newadr = $this->mUserEmail;
+                       $oldadr = $wgUser->getEmail();
+                       if( ($newadr != '') && ($newadr != $oldadr) ) {
+                               # the user has supplied a new email address on the login page
+                               if( $wgUser->isValidEmailAddr( $newadr ) ) {
+                                       # new behaviour: set this new emailaddr from login-page into user database record
+                                       $wgUser->setEmail( $newadr );
+                                       # but flag as "dirty" = unauthenticated
+                                       $wgUser->invalidateEmail();
+                                       if ($wgEmailAuthentication) {
+                                               # Mail a temporary password to the dirty address.
+                                               # User can come back through the confirmation URL to re-enable email.
+                                               $result = $wgUser->sendConfirmationMail();
+                                               if( WikiError::isError( $result ) ) {
+                                                       $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
+                                               } else {
+                                                       $error = wfMsg( 'eauthentsent', $wgUser->getName() );
+                                               }
+                                       }
+                               } else {
+                                       $error = wfMsg( 'invalidemailaddress' );
+                               }
+                       } else {
+                               if( $wgEmailConfirmToEdit && empty( $newadr ) ) {
+                                       $this->mainPrefsForm( 'error', wfMsg( 'noemailtitle' ) );
+                                       return;
+                               }
+                               $wgUser->setEmail( $this->mUserEmail );
+                       }
+                       if( $oldadr != $newadr ) {
+                               wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) );
+                       }
+               }
+
+               if( !$wgAuth->updateExternalDB( $wgUser ) ){
+                       $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
+                       return;
+               }
+
+               $msg = '';
+               if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) {
+                       $this->mainPrefsForm( 'error', $msg );
+                       return;
+               }
+
+               $wgUser->setCookies();
+               $wgUser->saveSettings();
+
+               if( $needRedirect && $error === false ) {
+                       $title = SpecialPage::getTitleFor( 'Preferences' );
+                       $wgOut->redirect( $title->getFullURL( 'success' ) );
+                       return;
+               }
+
+               $wgOut->parserOptions( ParserOptions::newFromUser( $wgUser ) );
+               $this->mainPrefsForm( $error === false ? 'success' : 'error', $error);
+       }
+
+       /**
+        * @access private
+        */
+       function resetPrefs() {
+               global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName;
+
+               $this->mOldpass = $this->mNewpass = $this->mRetypePass = '';
+               $this->mUserEmail = $wgUser->getEmail();
+               $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
+               $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
+
+               # language value might be blank, default to content language
+               $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode );
+
+               $this->mUserVariant = $wgUser->getOption( 'variant');
+               $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0;
+               $this->mNick = $wgUser->getOption( 'nickname' );
+
+               $this->mQuickbar = $wgUser->getOption( 'quickbar' );
+               $this->mSkin = Skin::normalizeKey( $wgUser->getOption( 'skin' ) );
+               $this->mMath = $wgUser->getOption( 'math' );
+               $this->mDate = $wgUser->getDatePreference();
+               $this->mRows = $wgUser->getOption( 'rows' );
+               $this->mCols = $wgUser->getOption( 'cols' );
+               $this->mStubs = $wgUser->getOption( 'stubthreshold' );
+               $this->mHourDiff = $wgUser->getOption( 'timecorrection' );
+               $this->mSearch = $wgUser->getOption( 'searchlimit' );
+               $this->mSearchLines = $wgUser->getOption( 'contextlines' );
+               $this->mSearchChars = $wgUser->getOption( 'contextchars' );
+               $this->mImageSize = $wgUser->getOption( 'imagesize' );
+               $this->mThumbSize = $wgUser->getOption( 'thumbsize' );
+               $this->mRecent = $wgUser->getOption( 'rclimit' );
+               $this->mRecentDays = $wgUser->getOption( 'rcdays' );
+               $this->mWatchlistEdits = $wgUser->getOption( 'wllimit' );
+               $this->mUnderline = $wgUser->getOption( 'underline' );
+               $this->mWatchlistDays = $wgUser->getOption( 'watchlistdays' );
+               $this->mUseAjaxSearch = $wgUser->getBoolOption( 'ajaxsearch' );
+               $this->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
+
+               $togs = User::getToggles();
+               foreach ( $togs as $tname ) {
+                       $this->mToggles[$tname] = $wgUser->getOption( $tname );
+               }
+
+               $namespaces = $wgContLang->getNamespaces();
+               foreach ( $namespaces as $i => $namespace ) {
+                       if ( $i >= NS_MAIN ) {
+                               $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i );
+                       }
+               }
+
+               wfRunHooks( 'ResetPreferences', array( $this, $wgUser ) );
+       }
+
+       /**
+        * @access private
+        */
+       function namespacesCheckboxes() {
+               global $wgContLang;
+
+               # Determine namespace checkboxes
+               $namespaces = $wgContLang->getNamespaces();
+               $r1 = null;
+
+               foreach ( $namespaces as $i => $name ) {
+                       if ($i < 0)
+                               continue;
+                       $checked = $this->mSearchNs[$i] ? "checked='checked'" : '';
+                       $name = str_replace( '_', ' ', $namespaces[$i] );
+
+                       if ( empty($name) )
+                               $name = wfMsg( 'blanknamespace' );
+
+                       $r1 .= "<input type='checkbox' value='1' name='wpNs$i' id='wpNs$i' {$checked}/> <label for='wpNs$i'>{$name}</label><br />\n";
+               }
+               return $r1;
+       }
+
+
+       function getToggle( $tname, $trailer = false, $disabled = false ) {
+               global $wgUser, $wgLang;
+
+               $this->mUsedToggles[$tname] = true;
+               $ttext = $wgLang->getUserToggle( $tname );
+
+               $checked = $wgUser->getOption( $tname ) == 1 ? ' checked="checked"' : '';
+               $disabled = $disabled ? ' disabled="disabled"' : '';
+               $trailer = $trailer ? $trailer : '';
+               return "<div class='toggle'><input type='checkbox' value='1' id=\"$tname\" name=\"wpOp$tname\"$checked$disabled />" .
+                       " <span class='toggletext'><label for=\"$tname\">$ttext</label>$trailer</span></div>\n";
+       }
+
+       function getToggles( $items ) {
+               $out = "";
+               foreach( $items as $item ) {
+                       if( $item === false )
+                               continue;
+                       if( is_array( $item ) ) {
+                               list( $key, $trailer ) = $item;
+                       } else {
+                               $key = $item;
+                               $trailer = false;
+                       }
+                       $out .= $this->getToggle( $key, $trailer );
+               }
+               return $out;
+       }
+
+       function addRow($td1, $td2) {
+               return "<tr><td class='mw-label'>$td1</td><td class='mw-input'>$td2</td></tr>";
+       }
+
+       /**
+        * Helper function for user information panel
+        * @param $td1 label for an item
+        * @param $td2 item or null
+        * @param $td3 optional help or null
+        * @return xhtml block
+        */
+       function tableRow( $td1, $td2 = null, $td3 = null ) {
+               global $wgContLang;
+
+               $align['align'] = $wgContLang->isRtl() ? 'right' : 'left';
+
+               if ( is_null( $td3 ) ) {
+                       $td3 = '';
+               } else {
+                       $td3 = Xml::tags( 'tr', null,
+                               Xml::tags( 'td', array( 'colspan' => '2' ), $td3 )
+                       );
+               }
+
+               if ( is_null( $td2 ) ) {
+                       $td1 = Xml::tags( 'td', $align + array( 'colspan' => '2' ), $td1 );
+                       $td2 = '';
+               } else {
+                       $td1 = Xml::tags( 'td', $align, $td1 );
+                       $td2 = Xml::tags( 'td', $align, $td2 );
+               }
+
+               return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n";
+
+       }
+
+       /**
+        * @access private
+        */
+       function mainPrefsForm( $status , $message = '' ) {
+               global $wgUser, $wgOut, $wgLang, $wgContLang;
+               global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
+               global $wgDisableLangConversion;
+               global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
+               global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
+               global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
+               global $wgContLanguageCode, $wgDefaultSkin, $wgSkipSkins, $wgAuth;
+               global $wgEmailConfirmToEdit, $wgAjaxSearch, $wgEnableMWSuggest;
+
+               $wgOut->setPageTitle( wfMsg( 'preferences' ) );
+               $wgOut->setArticleRelated( false );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+               $wgOut->addScriptFile( 'prefs.js' );
+
+               $wgOut->disallowUserJs();  # Prevent hijacked user scripts from sniffing passwords etc.
+
+               if ( $this->mSuccess || 'success' == $status ) {
+                       $wgOut->wrapWikiMsg( '<div class="successbox"><strong>$1</strong></div>', 'savedprefs' );
+               } else  if ( 'error' == $status ) {
+                       $wgOut->addWikiText( '<div class="errorbox"><strong>' . $message  . '</strong></div>' );
+               } else if ( '' != $status ) {
+                       $wgOut->addWikiText( $message . "\n----" );
+               }
+
+               $qbs = $wgLang->getQuickbarSettings();
+               $skinNames = $wgLang->getSkinNames();
+               $mathopts = $wgLang->getMathNames();
+               $dateopts = $wgLang->getDatePreferences();
+               $togs = User::getToggles();
+
+               $titleObj = SpecialPage::getTitleFor( 'Preferences' );
+               $action = $titleObj->escapeLocalURL();
+
+               # Pre-expire some toggles so they won't show if disabled
+               $this->mUsedToggles[ 'shownumberswatching' ] = true;
+               $this->mUsedToggles[ 'showupdated' ] = true;
+               $this->mUsedToggles[ 'enotifwatchlistpages' ] = true;
+               $this->mUsedToggles[ 'enotifusertalkpages' ] = true;
+               $this->mUsedToggles[ 'enotifminoredits' ] = true;
+               $this->mUsedToggles[ 'enotifrevealaddr' ] = true;
+               $this->mUsedToggles[ 'ccmeonemails' ] = true;
+               $this->mUsedToggles[ 'uselivepreview' ] = true;
+
+
+               if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
+               else { $emfc = ''; }
+
+
+               if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
+                       if( $wgUser->getEmailAuthenticationTimestamp() ) {
+                               $emailauthenticated = wfMsg('emailauthenticated',$wgLang->timeanddate($wgUser->getEmailAuthenticationTimestamp(), true ) ).'<br />';
+                               $disableEmailPrefs = false;
+                       } else {
+                               $disableEmailPrefs = true;
+                               $skin = $wgUser->getSkin();
+                               $emailauthenticated = wfMsg('emailnotauthenticated').'<br />' .
+                                       $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ),
+                                               wfMsg( 'emailconfirmlink' ) ) . '<br />';
+                       }
+               } else {
+                       $emailauthenticated = '';
+                       $disableEmailPrefs = false;
+               }
+
+               if ($this->mUserEmail == '') {
+                       $emailauthenticated = wfMsg( 'noemailprefs' ) . '<br />';
+               }
+
+               $ps = $this->namespacesCheckboxes();
+
+               $enotifwatchlistpages = ($wgEnotifWatchlist) ? $this->getToggle( 'enotifwatchlistpages', false, $disableEmailPrefs ) : '';
+               $enotifusertalkpages = ($wgEnotifUserTalk) ? $this->getToggle( 'enotifusertalkpages', false, $disableEmailPrefs ) : '';
+               $enotifminoredits = ($wgEnotifWatchlist && $wgEnotifMinorEdits) ? $this->getToggle( 'enotifminoredits', false, $disableEmailPrefs ) : '';
+               $enotifrevealaddr = (($wgEnotifWatchlist || $wgEnotifUserTalk) && $wgEnotifRevealEditorAddress) ? $this->getToggle( 'enotifrevealaddr', false, $disableEmailPrefs ) : '';
+
+               # </FIXME>
+
+               $wgOut->addHTML( "<form action=\"$action\" method='post'>" );
+               $wgOut->addHTML( "<div id='preferences'>" );
+
+               # User data
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'fieldset ' ) .
+                       Xml::element( 'legend', null, wfMsg('prefs-personal') ) .
+                       Xml::openElement( 'table' ) .
+                       $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) )
+               );
+
+               # Get groups to which the user belongs
+               $userEffectiveGroups = $wgUser->getEffectiveGroups();
+               $userEffectiveGroupsArray = array();
+               foreach( $userEffectiveGroups as $ueg ) {
+                       if( $ueg == '*' ) {
+                               // Skip the default * group, seems useless here
+                               continue;
+                       }
+                       $userEffectiveGroupsArray[] = User::makeGroupLinkHTML( $ueg );
+               }
+               asort( $userEffectiveGroupsArray );
+
+               $sk = $wgUser->getSkin();
+               $toolLinks = array();
+               $toolLinks[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'ListGroupRights' ), wfMsg( 'listgrouprights' ) );
+               # At the moment one tool link only but be prepared for the future...
+               # FIXME: Add a link to Special:Userrights for users who are allowed to use it. 
+               # $wgUser->isAllowed( 'userrights' ) seems to strict in some cases
+
+               $userInformationHtml =
+                       $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) .
+                       $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getId() ) ) .
+
+                       $this->tableRow(
+                               wfMsgExt( 'prefs-memberingroups', array( 'parseinline' ), count( $userEffectiveGroupsArray ) ),
+                               implode( wfMsg( 'comma-separator' ), $userEffectiveGroupsArray ) . 
+                               '<br />(' . implode( ' | ', $toolLinks ) . ')'
+                       ) .
+
+                       $this->tableRow(
+                               wfMsgHtml( 'prefs-edits' ),
+                               $wgLang->formatNum( User::edits( $wgUser->getId() ) )
+                       );
+
+               if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) {
+                       $wgOut->addHtml( $userInformationHtml );
+               }
+
+               if ( $wgAllowRealName ) {
+                       $wgOut->addHTML(
+                               $this->tableRow(
+                                       Xml::label( wfMsg('yourrealname'), 'wpRealName' ),
+                                       Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ),
+                                       Xml::tags('div', array( 'class' => 'prefsectiontip' ),
+                                               wfMsgExt( 'prefs-help-realname', 'parseinline' )
+                                       )
+                               )
+                       );
+               }
+               if ( $wgEnableEmail ) {
+                       $wgOut->addHTML(
+                               $this->tableRow(
+                                       Xml::label( wfMsg('youremail'), 'wpUserEmail' ),
+                                       Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ),
+                                       Xml::tags('div', array( 'class' => 'prefsectiontip' ),
+                                               wfMsgExt( $wgEmailConfirmToEdit ? 'prefs-help-email-required' : 'prefs-help-email', 'parseinline' )
+                                       )
+                               )
+                       );
+               }
+
+               global $wgParser, $wgMaxSigChars;
+               if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) {
+                       $invalidSig = $this->tableRow(
+                               '&nbsp;',
+                               Xml::element( 'span', array( 'class' => 'error' ),
+                                       wfMsgExt( 'badsiglength', 'parsemag', $wgLang->formatNum( $wgMaxSigChars ) ) )
+                       );
+               } elseif( !empty( $this->mToggles['fancysig'] ) &&
+                       false === $wgParser->validateSig( $this->mNick ) ) {
+                       $invalidSig = $this->tableRow(
+                               '&nbsp;',
+                               Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) )
+                       );
+               } else {
+                       $invalidSig = '';
+               }
+
+               $wgOut->addHTML(
+                       $this->tableRow(
+                               Xml::label( wfMsg( 'yournick' ), 'wpNick' ),
+                               Xml::input( 'wpNick', 25, $this->mNick,
+                                       array(
+                                               'id' => 'wpNick',
+                                               // Note: $wgMaxSigChars is enforced in Unicode characters,
+                                               // both on the backend and now in the browser.
+                                               // Badly-behaved requests may still try to submit
+                                               // an overlong string, however.
+                                               'maxlength' => $wgMaxSigChars ) )
+                       ) .
+                       $invalidSig .
+                       $this->tableRow( '&nbsp;', $this->getToggle( 'fancysig' ) )
+               );
+
+               list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage );
+               $wgOut->addHTML(
+                       $this->tableRow( $lsLabel, $lsSelect )
+               );
+
+               /* see if there are multiple language variants to choose from*/
+               if(!$wgDisableLangConversion) {
+                       $variants = $wgContLang->getVariants();
+                       $variantArray = array();
+
+                       $languages = Language::getLanguageNames( true );
+                       foreach($variants as $v) {
+                               $v = str_replace( '_', '-', strtolower($v));
+                               if( array_key_exists( $v, $languages ) ) {
+                                       // If it doesn't have a name, we'll pretend it doesn't exist
+                                       $variantArray[$v] = $languages[$v];
+                               }
+                       }
+
+                       $options = "\n";
+                       foreach( $variantArray as $code => $name ) {
+                               $selected = ($code == $this->mUserVariant);
+                               $options .= Xml::option( "$code - $name", $code, $selected ) . "\n";
+                       }
+
+                       if(count($variantArray) > 1) {
+                               $wgOut->addHtml(
+                                       $this->tableRow(
+                                               Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ),
+                                               Xml::tags( 'select',
+                                                       array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ),
+                                                       $options
+                                               )
+                                       )
+                               );
+                       }
+               }
+
+               # Password
+               if( $wgAuth->allowPasswordChange() ) {
+                       $wgOut->addHTML(
+                               $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) .
+                               $this->tableRow(
+                                       Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ),
+                                       Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) )
+                               ) .
+                               $this->tableRow(
+                                       Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ),
+                                       Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) )
+                               ) .
+                               $this->tableRow(
+                                       Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ),
+                                       Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) )
+                               ) .
+                               Xml::tags( 'tr', null,
+                                       Xml::tags( 'td', array( 'colspan' => '2' ),
+                                               $this->getToggle( "rememberpassword" )
+                                       )
+                               )
+                       );
+               }
+
+               # <FIXME>
+               # Enotif
+               if ( $wgEnableEmail ) {
+
+                       $moreEmail = '';
+                       if ($wgEnableUserEmail) {
+                               // fixme -- the "allowemail" pseudotoggle is a hacked-together
+                               // inversion for the "disableemail" preference.
+                               $emf = wfMsg( 'allowemail' );
+                               $disabled = $disableEmailPrefs ? ' disabled="disabled"' : '';
+                               $moreEmail =
+                                       "<input type='checkbox' $emfc $disabled value='1' name='wpEmailFlag' id='wpEmailFlag' /> <label for='wpEmailFlag'>$emf</label>" .
+                                       $this->getToggle( 'ccmeonemails', '', $disableEmailPrefs );
+                       }
+
+
+                       $wgOut->addHTML(
+                               $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) .
+                               $this->tableRow(
+                                       $emailauthenticated.
+                                       $enotifrevealaddr.
+                                       $enotifwatchlistpages.
+                                       $enotifusertalkpages.
+                                       $enotifminoredits.
+                                       $moreEmail
+                               )
+                       );
+               }
+               # </FIXME>
+
+               $wgOut->addHTML(
+                       Xml::closeElement( 'table' ) .
+                       Xml::closeElement( 'fieldset' )
+               );
+
+
+               # Quickbar
+               #
+               if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') {
+                       $wgOut->addHtml( "<fieldset>\n<legend>" . wfMsg( 'qbsettings' ) . "</legend>\n" );
+                       for ( $i = 0; $i < count( $qbs ); ++$i ) {
+                               if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; }
+                               else { $checked = ""; }
+                               $wgOut->addHTML( "<div><label><input type='radio' name='wpQuickbar' value=\"$i\"$checked />{$qbs[$i]}</label></div>\n" );
+                       }
+                       $wgOut->addHtml( "</fieldset>\n\n" );
+               } else {
+                       # Need to output a hidden option even if the relevant skin is not in use,
+                       # otherwise the preference will get reset to 0 on submit
+                       $wgOut->addHtml( wfHidden( 'wpQuickbar', $this->mQuickbar ) );
+               }
+
+               # Skin
+               #
+               $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg('skin') . "</legend>\n" );
+               $mptitle = Title::newMainPage();
+               $previewtext = wfMsg('skinpreview');
+               # Only show members of Skin::getSkinNames() rather than
+               # $skinNames (skins is all skin names from Language.php)
+               $validSkinNames = Skin::getSkinNames();
+               # Sort by UI skin name. First though need to update validSkinNames as sometimes
+               # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
+               foreach ($validSkinNames as $skinkey => & $skinname ) {
+                       if ( isset( $skinNames[$skinkey] ) )  {
+                               $skinname = $skinNames[$skinkey];
+                       }
+               }
+               asort($validSkinNames);
+               foreach ($validSkinNames as $skinkey => $sn ) {
+                       if ( in_array( $skinkey, $wgSkipSkins ) ) {
+                               continue;
+                       }
+                       $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
+
+                       $mplink = htmlspecialchars($mptitle->getLocalURL("useskin=$skinkey"));
+                       $previewlink = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
+                       if( $skinkey == $wgDefaultSkin )
+                               $sn .= ' (' . wfMsg( 'default' ) . ')';
+                       $wgOut->addHTML( "<input type='radio' name='wpSkin' id=\"wpSkin$skinkey\" value=\"$skinkey\"$checked /> <label for=\"wpSkin$skinkey\">{$sn}</label> $previewlink<br />\n" );
+               }
+               $wgOut->addHTML( "</fieldset>\n\n" );
+
+               # Math
+               #
+               global $wgUseTeX;
+               if( $wgUseTeX ) {
+                       $wgOut->addHTML( "<fieldset>\n<legend>" . wfMsg('math') . '</legend>' );
+                       foreach ( $mathopts as $k => $v ) {
+                               $checked = ($k == $this->mMath);
+                               $wgOut->addHTML(
+                                       Xml::openElement( 'div' ) .
+                                       Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) .
+                                       Xml::closeElement( 'div' ) . "\n"
+                               );
+                       }
+                       $wgOut->addHTML( "</fieldset>\n\n" );
+               }
+
+               # Files
+               #
+               $wgOut->addHTML(
+                       "<fieldset>\n" . Xml::element( 'legend', null, wfMsg( 'files' ) ) . "\n"
+               );
+
+               $imageLimitOptions = null;
+               foreach ( $wgImageLimits as $index => $limits ) {
+                       $selected = ($index == $this->mImageSize);
+                       $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" .
+                               wfMsg('unit-pixel'), $index, $selected );
+               }
+
+               $imageSizeId = 'wpImageSize';
+               $wgOut->addHTML(
+                       "<div>" . Xml::label( wfMsg('imagemaxsize'), $imageSizeId ) . " " .
+                       Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) .
+                               $imageLimitOptions .
+                       Xml::closeElement( 'select' ) . "</div>\n"
+               );
+
+               $imageThumbOptions = null;
+               foreach ( $wgThumbLimits as $index => $size ) {
+                       $selected = ($index == $this->mThumbSize);
+                       $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index,
+                               $selected);
+               }
+
+               $thumbSizeId = 'wpThumbSize';
+               $wgOut->addHTML(
+                       "<div>" . Xml::label( wfMsg('thumbsize'), $thumbSizeId ) . " " .
+                       Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
+                               $imageThumbOptions .
+                       Xml::closeElement( 'select' ) . "</div>\n"
+               );
+
+               $wgOut->addHTML( "</fieldset>\n\n" );
+
+               # Date format
+               #
+               # Date/Time
+               #
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'datetime' ) ) . "\n"
+               );
+
+               if ($dateopts) {
+                       $wgOut->addHTML(
+                               Xml::openElement( 'fieldset' ) .
+                               Xml::element( 'legend', null, wfMsg( 'dateformat' ) ) . "\n"
+                       );
+                       $idCnt = 0;
+                       $epoch = '20010115161234'; # Wikipedia day
+                       foreach( $dateopts as $key ) {
+                               if( $key == 'default' ) {
+                                       $formatted = wfMsg( 'datedefault' );
+                               } else {
+                                       $formatted = $wgLang->timeanddate( $epoch, false, $key );
+                               }
+                               $wgOut->addHTML(
+                                       Xml::tags( 'div', null,
+                                               Xml::radioLabel( $formatted, 'wpDate', $key, "wpDate$idCnt", $key == $this->mDate )
+                                       ) . "\n"
+                               );
+                               $idCnt++;
+                       }
+                       $wgOut->addHTML( Xml::closeElement( 'fieldset' ) . "\n" );
+               }
+
+               $nowlocal = $wgLang->time( $now = wfTimestampNow(), true );
+               $nowserver = $wgLang->time( $now, false );
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) .
+                       Xml::openElement( 'table' ) .
+                       $this->addRow( wfMsg( 'servertime' ), $nowserver ) .
+                       $this->addRow( wfMsg( 'localtime' ), $nowlocal ) .
+                       $this->addRow(
+                               Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff'  ),
+                               Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array( 'id' => 'wpHourDiff' ) ) ) .
+                       "<tr>
+                               <td></td>
+                               <td class='mw-submit'>" .
+                                       Xml::element( 'input',
+                                               array( 'type' => 'button',
+                                                       'value' => wfMsg( 'guesstimezone' ),
+                                                       'onclick' => 'javascript:guessTimezone()',
+                                                       'id' => 'guesstimezonebutton',
+                                                       'style' => 'display:none;' ) ) .
+                               "</td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::tags( 'div', array( 'class' => 'prefsectiontip' ), wfMsgExt( 'timezonetext', 'parseinline' ) ).
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'fieldset' ) . "\n\n"
+               );
+
+               # Editing
+               #
+               global $wgLivePreview;
+               $wgOut->addHTML( '<fieldset><legend>' . wfMsg( 'textboxsize' ) . '</legend>
+                       <div>' .
+                               wfInputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) .
+                               ' ' .
+                               wfInputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
+                       "</div>" .
+                       $this->getToggles( array(
+                               'editsection',
+                               'editsectiononrightclick',
+                               'editondblclick',
+                               'editwidth',
+                               'showtoolbar',
+                               'previewonfirst',
+                               'previewontop',
+                               'minordefault',
+                               'externaleditor',
+                               'externaldiff',
+                               $wgLivePreview ? 'uselivepreview' : false,
+                               'forceeditsummary',
+                       ) ) . '</fieldset>'
+               );
+
+               # Recent changes
+               $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-rc' ) . '</legend>' );
+
+               $rc  = '<table><tr>';
+               $rc .= '<td>' . Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . '</td>';
+               $rc .= '<td>' . Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . '</td>';
+               $rc .= '</tr><tr>';
+               $rc .= '<td>' . Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . '</td>';
+               $rc .= '<td>' . Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . '</td>';
+               $rc .= '</tr></table>';
+               $wgOut->addHtml( $rc );
+
+               $wgOut->addHtml( '<br />' );
+
+               $toggles[] = 'hideminor';
+               if( $wgRCShowWatchingUsers )
+                       $toggles[] = 'shownumberswatching';
+               $toggles[] = 'usenewrc';
+               $wgOut->addHtml( $this->getToggles( $toggles ) );
+
+               $wgOut->addHtml( '</fieldset>' );
+
+               # Watchlist
+               $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'prefs-watchlist' ) . '</legend>' );
+
+               $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) );
+               $wgOut->addHtml( '<br /><br />' );
+
+               $wgOut->addHtml( $this->getToggle( 'extendwatchlist' ) );
+               $wgOut->addHtml( wfInputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) );
+               $wgOut->addHtml( '<br /><br />' );
+
+               $wgOut->addHtml( $this->getToggles( array( 'watchlisthideown', 'watchlisthidebots', 'watchlisthideminor' ) ) );
+
+               if( $wgUser->isAllowed( 'createpage' ) || $wgUser->isAllowed( 'createtalk' ) )
+                       $wgOut->addHtml( $this->getToggle( 'watchcreations' ) );
+               foreach( array( 'edit' => 'watchdefault', 'move' => 'watchmoves', 'delete' => 'watchdeletion' ) as $action => $toggle ) {
+                       if( $wgUser->isAllowed( $action ) )
+                               $wgOut->addHtml( $this->getToggle( $toggle ) );
+               }
+               $this->mUsedToggles['watchcreations'] = true;
+               $this->mUsedToggles['watchdefault'] = true;
+               $this->mUsedToggles['watchmoves'] = true;
+               $this->mUsedToggles['watchdeletion'] = true;
+
+               $wgOut->addHtml( '</fieldset>' );
+
+               # Search
+               $ajaxsearch = $wgAjaxSearch ?
+                       $this->addRow(
+                               Xml::label( wfMsg( 'useajaxsearch' ), 'wpUseAjaxSearch' ),
+                               Xml::check( 'wpUseAjaxSearch', $this->mUseAjaxSearch, array( 'id' => 'wpUseAjaxSearch' ) )
+                       ) : '';
+               $mwsuggest = $wgEnableMWSuggest ?
+                       $this->addRow(
+                               Xml::label( wfMsg( 'mwsuggest-disable' ), 'wpDisableMWSuggest' ),
+                               Xml::check( 'wpDisableMWSuggest', $this->mDisableMWSuggest, array( 'id' => 'wpDisableMWSuggest' ) )
+                       ) : '';
+               $wgOut->addHTML(
+                       // Elements for the search tab itself
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'searchresultshead' ) ) .
+                       // Elements for the search options in the search tab
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'prefs-searchoptions' ) ) .
+                       Xml::openElement( 'table' ) .
+                       $ajaxsearch .
+                       $this->addRow(
+                               Xml::label( wfMsg( 'resultsperpage' ), 'wpSearch' ),
+                               Xml::input( 'wpSearch', 4, $this->mSearch, array( 'id' => 'wpSearch' ) )
+                       ) .
+                       $this->addRow(
+                               Xml::label( wfMsg( 'contextlines' ), 'wpSearchLines' ),
+                               Xml::input( 'wpSearchLines', 4, $this->mSearchLines, array( 'id' => 'wpSearchLines' ) )
+                       ) .
+                       $this->addRow(
+                               Xml::label( wfMsg( 'contextchars' ), 'wpSearchChars' ),
+                               Xml::input( 'wpSearchChars', 4, $this->mSearchChars, array( 'id' => 'wpSearchChars' ) )
+                       ) .
+                       $mwsuggest .
+                       Xml::closeElement( 'table' ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       // Elements for the namespace options in the search tab
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'prefs-namespaces' ) ) .
+                       wfMsgExt( 'defaultns', array( 'parse' ) ) .
+                       $ps .
+                       Xml::closeElement( 'fieldset' ) .
+                       // End of the search tab
+                       Xml::closeElement( 'fieldset' )
+               );
+
+               # Misc
+               #
+               $wgOut->addHTML('<fieldset><legend>' . wfMsg('prefs-misc') . '</legend>');
+               $wgOut->addHtml( '<label for="wpStubs">' . wfMsg( 'stub-threshold' ) . '</label>&nbsp;' );
+               $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) );
+               $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) );
+               $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) );
+               $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) );
+               $msgUnderlinedefault = htmlspecialchars( wfMsg ( 'underline-default' ) );
+               $uopt = $wgUser->getOption("underline");
+               $s0 = $uopt == 0 ? ' selected="selected"' : '';
+               $s1 = $uopt == 1 ? ' selected="selected"' : '';
+               $s2 = $uopt == 2 ? ' selected="selected"' : '';
+               $wgOut->addHTML("
+<div class='toggle'><p><label for='wpOpunderline'>$msgUnderline</label>
+<select name='wpOpunderline' id='wpOpunderline'>
+<option value=\"0\"$s0>$msgUnderlinenever</option>
+<option value=\"1\"$s1>$msgUnderlinealways</option>
+<option value=\"2\"$s2>$msgUnderlinedefault</option>
+</select></p></div>");
+
+               foreach ( $togs as $tname ) {
+                       if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
+                               $wgOut->addHTML( $this->getToggle( $tname ) );
+                       }
+               }
+               $wgOut->addHTML( '</fieldset>' );
+
+               wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
+
+               $token = htmlspecialchars( $wgUser->editToken() );
+               $skin = $wgUser->getSkin();
+               $wgOut->addHTML( "
+       <div id='prefsubmit'>
+       <div>
+               <input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) . '"'.$skin->tooltipAndAccesskey('save')." />
+               <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" />
+       </div>
+
+       </div>
+
+       <input type='hidden' name='wpEditToken' value=\"{$token}\" />
+       </div></form>\n" );
+
+               $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ),
+                       wfMsgExt( 'clearyourcache', 'parseinline' ) )
+               );
+       }
+}
diff --git a/includes/specials/Prefixindex.php b/includes/specials/Prefixindex.php
new file mode 100644 (file)
index 0000000..1819d4e
--- /dev/null
@@ -0,0 +1,152 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point : initialise variables and call subfunctions.
+ * @param $par String: becomes "FOO" when called like Special:Prefixindex/FOO (default NULL)
+ * @param $specialPage SpecialPage object.
+ */
+function wfSpecialPrefixIndex( $par=NULL, $specialPage ) {
+       global $wgRequest, $wgOut, $wgContLang;
+
+       # GET values
+       $from = $wgRequest->getVal( 'from' );
+       $prefix = $wgRequest->getVal( 'prefix' );
+       $namespace = $wgRequest->getInt( 'namespace' );
+       $namespaces = $wgContLang->getNamespaces();
+
+       $indexPage = new SpecialPrefixIndex();
+
+       $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) )
+               ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) )
+               : wfMsg( 'allarticles' )
+       );
+
+       if ( isset($par) ) {
+               $indexPage->showChunk( $namespace, $par, $specialPage->including(), $from );
+       } elseif ( isset($prefix) ) {
+               $indexPage->showChunk( $namespace, $prefix, $specialPage->including(), $from );
+       } elseif ( isset($from) ) {
+               $indexPage->showChunk( $namespace, $from, $specialPage->including(), $from );
+       } else {
+               $wgOut->addHtml($indexPage->namespaceForm ( $namespace, null ));
+       }
+}
+
+/**
+ * implements Special:Prefixindex
+ * @ingroup SpecialPage
+ */
+class SpecialPrefixindex extends SpecialAllpages {
+       // Inherit $maxPerPage
+
+       // Define other properties
+       protected $name = 'Prefixindex';
+       protected $nsfromMsg = 'allpagesprefix';
+
+       /**
+        * @param integer $namespace (Default NS_MAIN)
+        * @param string $from list all pages from this name (default FALSE)
+        */
+       function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = null ) {
+               global $wgOut, $wgUser, $wgContLang;
+
+               $fname = 'indexShowChunk';
+
+               $sk = $wgUser->getSkin();
+
+               if (!isset($from)) $from = $prefix;
+
+               $fromList = $this->getNamespaceKeyAndText($namespace, $from);
+               $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix);
+               $namespaces = $wgContLang->getNamespaces();
+               $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+               if ( !$prefixList || !$fromList ) {
+                       $out = wfMsgWikiHtml( 'allpagesbadtitle' );
+               } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) {
+                       // Show errormessage and reset to NS_MAIN
+                       $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace );
+                       $namespace = NS_MAIN;
+               } else {
+                       list( $namespace, $prefixKey, $prefix ) = $prefixList;
+                       list( /* $fromNs */, $fromKey, $from ) = $fromList;
+
+                       ### FIXME: should complain if $fromNs != $namespace
+
+                       $dbr = wfGetDB( DB_SLAVE );
+
+                       $res = $dbr->select( 'page',
+                               array( 'page_namespace', 'page_title', 'page_is_redirect' ),
+                               array(
+                                       'page_namespace' => $namespace,
+                                       'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
+                                       'page_title >= ' . $dbr->addQuotes( $fromKey ),
+                               ),
+                               $fname,
+                               array(
+                                       'ORDER BY'  => 'page_title',
+                                       'LIMIT'     => $this->maxPerPage + 1,
+                                       'USE INDEX' => 'name_title',
+                               )
+                       );
+
+                       ### FIXME: side link to previous
+
+                       $n = 0;
+                       if( $res->numRows() > 0 ) {
+                               $out = '<table style="background: inherit;" border="0" width="100%">';
+       
+                               while( ($n < $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+                                       $t = Title::makeTitle( $s->page_namespace, $s->page_title );
+                                       if( $t ) {
+                                               $link = ($s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
+                                                       $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
+                                                       ($s->page_is_redirect ? '</div>' : '' );
+                                       } else {
+                                               $link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
+                                       }
+                                       if( $n % 3 == 0 ) {
+                                               $out .= '<tr>';
+                                       }
+                                       $out .= "<td>$link</td>";
+                                       $n++;
+                                       if( $n % 3 == 0 ) {
+                                               $out .= '</tr>';
+                                       }
+                               }
+                               if( ($n % 3) != 0 ) {
+                                       $out .= '</tr>';
+                               }
+                               $out .= '</table>';
+                       } else {
+                               $out = '';
+                       }
+               }
+
+               if ( $including ) {
+                       $out2 = '';
+               } else {
+                       $nsForm = $this->namespaceForm ( $namespace, $prefix );
+                       $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
+                       $out2 .= '<tr valign="top"><td>' . $nsForm;
+                       $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
+                                       $sk->makeKnownLink( $wgContLang->specialPage( $this->name ),
+                                               wfMsg ( 'allpages' ) );
+                       if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) {
+                               $namespaceparam = $namespace ? "&namespace=$namespace" : "";
+                               $out2 .= " | " . $sk->makeKnownLink(
+                                       $wgContLang->specialPage( $this->name ),
+                                       wfMsg ( 'nextpage', $s->page_title ),
+                                       "from=" . wfUrlEncode ( $s->page_title ) .
+                                       "&prefix=" . wfUrlEncode ( $prefix ) . $namespaceparam );
+                       }
+                       $out2 .= "</td></tr></table><hr />";
+               }
+
+               $wgOut->addHtml( $out2 . $out );
+       }
+}
diff --git a/includes/specials/Protectedpages.php b/includes/specials/Protectedpages.php
new file mode 100644 (file)
index 0000000..e168902
--- /dev/null
@@ -0,0 +1,313 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @ingroup SpecialPage
+ */
+class ProtectedPagesForm {
+
+       protected $IdLevel = 'level';
+       protected $IdType  = 'type';
+
+       public function showList( $msg = '' ) {
+               global $wgOut, $wgRequest;
+
+               $wgOut->setPagetitle( wfMsg( "protectedpages" ) );
+               if ( "" != $msg ) {
+                       $wgOut->setSubtitle( $msg );
+               }
+
+               // Purge expired entries on one in every 10 queries
+               if ( !mt_rand( 0, 10 ) ) {
+                       Title::purgeExpiredRestrictions();
+               }
+
+               $type = $wgRequest->getVal( $this->IdType );
+               $level = $wgRequest->getVal( $this->IdLevel );
+               $sizetype = $wgRequest->getVal( 'sizetype' );
+               $size = $wgRequest->getIntOrNull( 'size' );
+               $NS = $wgRequest->getIntOrNull( 'namespace' );
+               $indefOnly = $wgRequest->getBool( 'indefonly' ) ? 1 : 0;
+
+               $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size, $indefOnly );
+
+               $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size, $indefOnly ) );
+
+               if ( $pager->getNumRows() ) {
+                       $s = $pager->getNavigationBar();
+                       $s .= "<ul>" .
+                               $pager->getBody() .
+                               "</ul>";
+                       $s .= $pager->getNavigationBar();
+               } else {
+                       $s = '<p>' . wfMsgHtml( 'protectedpagesempty' ) . '</p>';
+               }
+               $wgOut->addHTML( $s );
+       }
+
+       /**
+        * Callback function to output a restriction
+        * @param $row object Protected title
+        * @return string Formatted <li> element
+        */
+       public function formatRow( $row ) {
+               global $wgUser, $wgLang, $wgContLang;
+
+               wfProfileIn( __METHOD__ );
+
+               static $skin=null;
+
+               if( is_null( $skin ) )
+                       $skin = $wgUser->getSkin();
+
+               $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+               $link = $skin->makeLinkObj( $title );
+
+               $description_items = array ();
+
+               $protType = wfMsgHtml( 'restriction-level-' . $row->pr_level );
+
+               $description_items[] = $protType;
+
+               if ( $row->pr_cascade ) {
+                       $description_items[] = wfMsg( 'protect-summary-cascade' );
+               }
+
+               $expiry_description = '';
+               $stxt = '';
+
+               if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) {
+                       $expiry = Block::decodeExpiry( $row->pr_expiry );
+
+                       $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+                       $description_items[] = $expiry_description;
+               }
+
+               if (!is_null($size = $row->page_len)) {
+                       if ($size == 0)
+                               $stxt = ' <small>' . wfMsgHtml('historyempty') . '</small>';
+                       else
+                               $stxt = ' <small>' . wfMsgHtml('historysize', $wgLang->formatNum( $size ) ) . '</small>';
+                       $stxt = $wgContLang->getDirMark() . $stxt;
+               }
+
+               # Show a link to the change protection form for allowed users otherwise a link to the protection log
+               if( $wgUser->isAllowed( 'protect' ) ) {
+                       $changeProtection = ' (' . $skin->makeKnownLinkObj( $title, wfMsgHtml( 'protect_change' ), 'action=unprotect' ) . ')';
+               } else {
+                       $ltitle = SpecialPage::getTitleFor( 'Log' );
+                       $changeProtection = ' (' . $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'protectlogpage' ), 'type=protect&page=' . $title->getPrefixedUrl() ) . ')';
+               }
+
+               wfProfileOut( __METHOD__ );
+
+               return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "</li>\n";
+       }
+
+       /**
+        * @param $namespace int
+        * @param $type string
+        * @param $level string
+        * @param $minsize int
+        * @param $indefOnly bool
+        * @return string Input form
+        * @private
+        */
+       protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly ) {
+               global $wgScript;
+               $title = SpecialPage::getTitleFor( 'ProtectedPages' );
+               return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
+                       Xml::hidden( 'title', $title->getPrefixedDBkey() ) . "&nbsp;\n" .
+                       $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+                       $this->getTypeMenu( $type ) . "&nbsp;\n" .
+                       $this->getLevelMenu( $level ) . "&nbsp;\n" .
+                       "<br /><span style='white-space: nowrap'>&nbsp;&nbsp;" .
+                       $this->getExpiryCheck( $indefOnly ) . "&nbsp;\n" .
+                       $this->getSizeLimit( $sizetype, $size ) . "&nbsp;\n" .
+                       "</span>" .
+                       "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' );
+       }
+
+       /**
+        * Prepare the namespace filter drop-down; standard namespace
+        * selector, sans the MediaWiki namespace
+        *
+        * @param mixed $namespace Pre-select namespace
+        * @return string
+        */
+       protected function getNamespaceMenu( $namespace = null ) {
+               return "<span style='white-space: nowrap'>" .
+                       Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;'
+                       . Xml::namespaceSelector( $namespace, '' ) . "</span>";
+       }
+
+       /**
+        * @return string Formatted HTML
+        */
+       protected function getExpiryCheck( $indefOnly ) {
+               return
+                       Xml::checkLabel( wfMsg('protectedpages-indef'), 'indefonly', 'indefonly', $indefOnly ) . "\n";
+       }
+
+       /**
+        * @return string Formatted HTML
+        */
+       protected function getSizeLimit( $sizetype, $size ) {
+               $max = $sizetype === 'max';
+
+               return
+                       Xml::radioLabel( wfMsg('minimum-size'), 'sizetype', 'min', 'wpmin', !$max ) .
+                       '&nbsp;' .
+                       Xml::radioLabel( wfMsg('maximum-size'), 'sizetype', 'max', 'wpmax', $max ) .
+                       '&nbsp;' .
+                       Xml::input( 'size', 9, $size, array( 'id' => 'wpsize' ) ) .
+                       '&nbsp;' .
+                       Xml::label( wfMsg('pagesize'), 'wpsize' );
+       }
+
+       /**
+        * @return string Formatted HTML
+        */
+       protected function getTypeMenu( $pr_type ) {
+               global $wgRestrictionTypes;
+
+               $m = array(); // Temporary array
+               $options = array();
+
+               // First pass to load the log names
+               foreach( $wgRestrictionTypes as $type ) {
+                       $text = wfMsg("restriction-$type");
+                       $m[$text] = $type;
+               }
+
+               // Third pass generates sorted XHTML content
+               foreach( $m as $text => $type ) {
+                       $selected = ($type == $pr_type );
+                       $options[] = Xml::option( $text, $type, $selected ) . "\n";
+               }
+
+               return "<span style='white-space: nowrap'>" .
+                       Xml::label( wfMsg('restriction-type') , $this->IdType ) . '&nbsp;' .
+                       Xml::tags( 'select',
+                               array( 'id' => $this->IdType, 'name' => $this->IdType ),
+                               implode( "\n", $options ) ) . "</span>";
+       }
+
+       /**
+        * @return string Formatted HTML
+        */
+       protected function getLevelMenu( $pr_level ) {
+               global $wgRestrictionLevels;
+
+               $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+               $options = array();
+
+               // First pass to load the log names
+               foreach( $wgRestrictionLevels as $type ) {
+                       if ( $type !='' && $type !='*') {
+                               $text = wfMsg("restriction-level-$type");
+                               $m[$text] = $type;
+                       }
+               }
+
+               // Third pass generates sorted XHTML content
+               foreach( $m as $text => $type ) {
+                       $selected = ($type == $pr_level );
+                       $options[] = Xml::option( $text, $type, $selected );
+               }
+
+               return
+                       Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+                       Xml::tags( 'select',
+                               array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+                               implode( "\n", $options ) );
+       }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class ProtectedPagesPager extends AlphabeticPager {
+       public $mForm, $mConds;
+       private $type, $level, $namespace, $sizetype, $size, $indefonly;
+
+       function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0, $indefonly=false ) {
+               $this->mForm = $form;
+               $this->mConds = $conds;
+               $this->type = ( $type ) ? $type : 'edit';
+               $this->level = $level;
+               $this->namespace = $namespace;
+               $this->sizetype = $sizetype;
+               $this->size = intval($size);
+               $this->indefonly = (bool)$indefonly;
+               parent::__construct();
+       }
+
+       function getStartBody() {
+               wfProfileIn( __METHOD__ );
+               # Do a link batch query
+               $lb = new LinkBatch;
+               while( $row = $this->mResult->fetchObject() ) {
+                       $lb->add( $row->page_namespace, $row->page_title );
+               }
+               $lb->execute();
+
+               wfProfileOut( __METHOD__ );
+               return '';
+       }
+
+       function formatRow( $row ) {
+               return $this->mForm->formatRow( $row );
+       }
+
+       function getQueryInfo() {
+               $conds = $this->mConds;
+               $conds[] = 'pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+               $conds[] = 'page_id=pr_page';
+               $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type );
+
+               if( $this->sizetype=='min' ) {
+                       $conds[] = 'page_len>=' . $this->size;
+               } else if( $this->sizetype=='max' ) {
+                       $conds[] = 'page_len<=' . $this->size;
+               }
+
+               if( $this->indefonly ) {
+                       $conds[] = "pr_expiry = 'infinity' OR pr_expiry IS NULL";
+               }
+
+               if( $this->level )
+                       $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level );
+               if( !is_null($this->namespace) )
+                       $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace );
+               return array(
+                       'tables' => array( 'page_restrictions', 'page' ),
+                       'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade',
+                       'conds' => $conds
+               );
+       }
+
+       function getIndexField() {
+               return 'pr_id';
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedpages() {
+
+       $ppForm = new ProtectedPagesForm();
+
+       $ppForm->showList();
+}
diff --git a/includes/specials/Protectedtitles.php b/includes/specials/Protectedtitles.php
new file mode 100644 (file)
index 0000000..2ec68a6
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @todo document
+ * @ingroup SpecialPage
+ */
+class ProtectedTitlesForm {
+
+       protected $IdLevel = 'level';
+       protected $IdType  = 'type';
+
+       function showList( $msg = '' ) {
+               global $wgOut, $wgRequest;
+
+               $wgOut->setPagetitle( wfMsg( "protectedtitles" ) );
+               if ( "" != $msg ) {
+                       $wgOut->setSubtitle( $msg );
+               }
+
+               // Purge expired entries on one in every 10 queries
+               if ( !mt_rand( 0, 10 ) ) {
+                       Title::purgeExpiredRestrictions();
+               }
+
+               $type = $wgRequest->getVal( $this->IdType );
+               $level = $wgRequest->getVal( $this->IdLevel );
+               $sizetype = $wgRequest->getVal( 'sizetype' );
+               $size = $wgRequest->getIntOrNull( 'size' );
+               $NS = $wgRequest->getIntOrNull( 'namespace' );
+
+               $pager = new ProtectedTitlesPager( $this, array(), $type, $level, $NS, $sizetype, $size );
+
+               $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) );
+
+               if ( $pager->getNumRows() ) {
+                       $s = $pager->getNavigationBar();
+                       $s .= "<ul>" .
+                               $pager->getBody() .
+                               "</ul>";
+                       $s .= $pager->getNavigationBar();
+               } else {
+                       $s = '<p>' . wfMsgHtml( 'protectedtitlesempty' ) . '</p>';
+               }
+               $wgOut->addHTML( $s );
+       }
+
+       /**
+        * Callback function to output a restriction
+        */
+       function formatRow( $row ) {
+               global $wgUser, $wgLang, $wgContLang;
+
+               wfProfileIn( __METHOD__ );
+
+               static $skin=null;
+
+               if( is_null( $skin ) )
+                       $skin = $wgUser->getSkin();
+
+               $title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
+               $link = $skin->makeLinkObj( $title );
+
+               $description_items = array ();
+
+               $protType = wfMsgHtml( 'restriction-level-' . $row->pt_create_perm );
+
+               $description_items[] = $protType;
+
+               $expiry_description = ''; $stxt = '';
+
+               if ( $row->pt_expiry != 'infinity' && strlen($row->pt_expiry) ) {
+                       $expiry = Block::decodeExpiry( $row->pt_expiry );
+
+                       $expiry_description = wfMsgForContent( 'protect-expiring', $wgLang->timeanddate( $expiry ) );
+
+                       $description_items[] = $expiry_description;
+               }
+
+               wfProfileOut( __METHOD__ );
+
+               return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . "</li>\n";
+       }
+
+       /**
+        * @param $namespace int
+        * @param $type string
+        * @param $level string
+        * @param $minsize int
+        * @private
+        */
+       function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
+               global $wgScript;
+               $action = htmlspecialchars( $wgScript );
+               $title = SpecialPage::getTitleFor( 'ProtectedTitles' );
+               $special = htmlspecialchars( $title->getPrefixedDBkey() );
+               return "<form action=\"$action\" method=\"get\">\n" .
+                       '<fieldset>' .
+                       Xml::element( 'legend', array(), wfMsg( 'protectedtitles' ) ) .
+                       Xml::hidden( 'title', $special ) . "&nbsp;\n" .
+                       $this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
+                       // $this->getLevelMenu( $level ) . "<br/>\n" .
+                       "&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+                       "</fieldset></form>";
+       }
+
+       /**
+        * Prepare the namespace filter drop-down; standard namespace
+        * selector, sans the MediaWiki namespace
+        *
+        * @param mixed $namespace Pre-select namespace
+        * @return string
+        */
+       function getNamespaceMenu( $namespace = null ) {
+               return Xml::label( wfMsg( 'namespace' ), 'namespace' )
+                       . '&nbsp;'
+                       . Xml::namespaceSelector( $namespace, '' );
+       }
+
+       /**
+        * @return string Formatted HTML
+        * @private
+        */
+       function getLevelMenu( $pr_level ) {
+               global $wgRestrictionLevels;
+
+               $m = array( wfMsg('restriction-level-all') => 0 ); // Temporary array
+               $options = array();
+
+               // First pass to load the log names
+               foreach( $wgRestrictionLevels as $type ) {
+                       if ( $type !='' && $type !='*') {
+                               $text = wfMsg("restriction-level-$type");
+                               $m[$text] = $type;
+                       }
+               }
+
+               // Third pass generates sorted XHTML content
+               foreach( $m as $text => $type ) {
+                       $selected = ($type == $pr_level );
+                       $options[] = Xml::option( $text, $type, $selected );
+               }
+
+               return
+                       Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+                       Xml::tags( 'select',
+                               array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
+                               implode( "\n", $options ) );
+       }
+}
+
+/**
+ * @todo document
+ * @ingroup Pager
+ */
+class ProtectedtitlesPager extends AlphabeticPager {
+       public $mForm, $mConds;
+
+       function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) {
+               $this->mForm = $form;
+               $this->mConds = $conds;
+               $this->level = $level;
+               $this->namespace = $namespace;
+               $this->size = intval($size);
+               parent::__construct();
+       }
+
+       function getStartBody() {
+               wfProfileIn( __METHOD__ );
+               # Do a link batch query
+               $this->mResult->seek( 0 );
+               $lb = new LinkBatch;
+
+               while ( $row = $this->mResult->fetchObject() ) {
+                       $lb->add( $row->pt_namespace, $row->pt_title );
+               }
+
+               $lb->execute();
+               wfProfileOut( __METHOD__ );
+               return '';
+       }
+
+       function formatRow( $row ) {
+               return $this->mForm->formatRow( $row );
+       }
+
+       function getQueryInfo() {
+               $conds = $this->mConds;
+               $conds[] = 'pt_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() );
+
+               if( !is_null($this->namespace) )
+                       $conds[] = 'pt_namespace=' . $this->mDb->addQuotes( $this->namespace );
+               return array(
+                       'tables' => 'protected_titles',
+                       'fields' => 'pt_namespace,pt_title,pt_create_perm,pt_expiry,pt_timestamp',
+                       'conds' => $conds
+               );
+       }
+
+       function getIndexField() {
+               return 'pt_timestamp';
+       }
+}
+
+/**
+ * Constructor
+ */
+function wfSpecialProtectedtitles() {
+
+       $ppForm = new ProtectedTitlesForm();
+
+       $ppForm->showList();
+}
diff --git a/includes/specials/Randompage.php b/includes/specials/Randompage.php
new file mode 100644 (file)
index 0000000..0e7ada1
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * Special page to direct the user to a random page
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
+ * @license GNU General Public Licence 2.0 or later
+ */
+class RandomPage extends SpecialPage {
+       private $namespace = NS_MAIN;  // namespace to select pages from
+
+       function __construct( $name = 'Randompage' ){
+               parent::__construct( $name );
+       }
+
+       public function getNamespace() {
+               return $this->namespace;
+       }
+
+       public function setNamespace ( $ns ) {
+               if( $ns < NS_MAIN ) $ns = NS_MAIN;
+               $this->namespace = $ns;
+       }
+
+       // select redirects instead of normal pages?
+       // Overriden by SpecialRandomredirect
+       public function isRedirect(){
+               return false;
+       }
+
+       public function execute( $par ) {
+               global $wgOut, $wgContLang;
+
+               if ($par)
+                       $this->setNamespace( $wgContLang->getNsIndex( $par ) );
+
+               $title = $this->getRandomTitle();
+
+               if( is_null( $title ) ) {
+                       $this->setHeaders();
+                       $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages' );
+                       return;
+               }
+
+               $query = $this->isRedirect() ? 'redirect=no' : '';
+               $wgOut->redirect( $title->getFullUrl( $query ) );
+       }
+
+
+       /**
+        * Choose a random title.
+        * @return Title object (or null if nothing to choose from)
+        */
+       public function getRandomTitle() {
+               $randstr = wfRandom();
+               $row = $this->selectRandomPageFromDB( $randstr );
+
+               /* If we picked a value that was higher than any in
+                * the DB, wrap around and select the page with the
+                * lowest value instead!  One might think this would
+                * skew the distribution, but in fact it won't cause
+                * any more bias than what the page_random scheme
+                * causes anyway.  Trust me, I'm a mathematician. :)
+                */
+               if( !$row )
+                       $row = $this->selectRandomPageFromDB( "0" );
+
+               if( $row )
+                       return Title::makeTitleSafe( $this->namespace, $row->page_title );
+               else
+                       return null;
+       }
+
+       private function selectRandomPageFromDB( $randstr ) {
+               global $wgExtraRandompageSQL;
+               $fname = 'RandomPage::selectRandomPageFromDB';
+
+               $dbr = wfGetDB( DB_SLAVE );
+
+               $use_index = $dbr->useIndexClause( 'page_random' );
+               $page = $dbr->tableName( 'page' );
+
+               $ns = (int) $this->namespace;
+               $redirect = $this->isRedirect() ? 1 : 0;
+
+               $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
+               $sql = "SELECT page_title
+                       FROM $page $use_index
+                       WHERE page_namespace = $ns
+                       AND page_is_redirect = $redirect
+                       AND page_random >= $randstr
+                       $extra
+                       ORDER BY page_random";
+
+               $sql = $dbr->limitResult( $sql, 1, 0 );
+               $res = $dbr->query( $sql, $fname );
+               return $dbr->fetchObject( $res );
+       }
+}
diff --git a/includes/specials/Randomredirect.php b/includes/specials/Randomredirect.php
new file mode 100644 (file)
index 0000000..629d5b3
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * Special page to direct the user to a random redirect page (minus the second redirect)
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>, Ilmari Karonen
+ * @license GNU General Public Licence 2.0 or later
+ */
+class SpecialRandomredirect extends RandomPage {
+       function __construct(){
+               parent::__construct( 'Randomredirect' );
+       }
+
+       // Override parent::isRedirect()
+       public function isRedirect(){
+               return true;
+       }
+}
diff --git a/includes/specials/Recentchanges.php b/includes/specials/Recentchanges.php
new file mode 100644 (file)
index 0000000..cd1dac0
--- /dev/null
@@ -0,0 +1,803 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( dirname(__FILE__) . '/ChangesList.php' );
+
+/**
+ * Constructor
+ */
+function wfSpecialRecentchanges( $par, $specialPage ) {
+       global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
+       global $wgRCShowWatchingUsers, $wgShowUpdatedMarker;
+       global $wgAllowCategorizedRecentChanges ;
+
+       # Get query parameters
+       $feedFormat = $wgRequest->getVal( 'feed' );
+
+       /* Checkbox values can't be true by default, because
+        * we cannot differentiate between unset and not set at all
+        */
+       $defaults = array(
+       /* int  */ 'days' => $wgUser->getDefaultOption('rcdays'),
+       /* int  */ 'limit' => $wgUser->getDefaultOption('rclimit'),
+       /* bool */ 'hideminor' => false,
+       /* bool */ 'hidebots' => true,
+       /* bool */ 'hideanons' => false,
+       /* bool */ 'hideliu' => false,
+       /* bool */ 'hidepatrolled' => false,
+       /* bool */ 'hidemyself' => false,
+       /* text */ 'from' => '',
+       /* text */ 'namespace' => null,
+       /* bool */ 'invert' => false,
+       /* bool */ 'categories_any' => false,
+       );
+
+       extract($defaults);
+
+
+       $days = $wgUser->getOption( 'rcdays', $defaults['days']);
+       $days = $wgRequest->getInt( 'days', $days );
+
+       $limit = $wgUser->getOption( 'rclimit', $defaults['limit'] );
+
+       #       list( $limit, $offset ) = wfCheckLimits( 100, 'rclimit' );
+       $limit = $wgRequest->getInt( 'limit', $limit );
+
+       /* order of selection: url > preferences > default */
+       $hideminor = $wgRequest->getBool( 'hideminor', $wgUser->getOption( 'hideminor') ? true : $defaults['hideminor'] );
+
+       # As a feed, use limited settings only
+       if( $feedFormat ) {
+               global $wgFeedLimit;
+               $limit = min( $wgFeedLimit, $limit );
+       } else {
+
+               $namespace = $wgRequest->getIntOrNull( 'namespace' );
+               $invert = $wgRequest->getBool( 'invert', $defaults['invert'] );
+               $hidebots = $wgRequest->getBool( 'hidebots', $defaults['hidebots'] );
+               $hideanons = $wgRequest->getBool( 'hideanons', $defaults['hideanons'] );
+               $hideliu = $wgRequest->getBool( 'hideliu', $defaults['hideliu'] );
+               $hidepatrolled = $wgRequest->getBool( 'hidepatrolled', $defaults['hidepatrolled'] );
+               $hidemyself = $wgRequest->getBool ( 'hidemyself', $defaults['hidemyself'] );
+               $from = $wgRequest->getVal( 'from', $defaults['from'] );
+
+               # Get query parameters from path
+               if( $par ) {
+                       $bits = preg_split( '/\s*,\s*/', trim( $par ) );
+                       foreach ( $bits as $bit ) {
+                               if ( 'hidebots' == $bit ) $hidebots = 1;
+                               if ( 'bots' == $bit ) $hidebots = 0;
+                               if ( 'hideminor' == $bit ) $hideminor = 1;
+                               if ( 'minor' == $bit ) $hideminor = 0;
+                               if ( 'hideliu' == $bit ) $hideliu = 1;
+                               if ( 'hidepatrolled' == $bit ) $hidepatrolled = 1;
+                               if ( 'hideanons' == $bit ) $hideanons = 1;
+                               if ( 'hidemyself' == $bit ) $hidemyself = 1;
+
+                               if ( is_numeric( $bit ) ) {
+                                       $limit = $bit;
+                               }
+
+                               $m = array();
+                               if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
+                                       $limit = $m[1];
+                               }
+
+                               if ( preg_match( '/^days=(\d+)$/', $bit, $m ) ) {
+                                       $days = $m[1];
+                               }
+                       }
+               }
+       }
+
+       if ( $limit < 0 || $limit > 5000 ) $limit = $defaults['limit'];
+
+       # Database connection and caching
+       $dbr = wfGetDB( DB_SLAVE );
+
+       $cutoff_unixtime = time() - ( $days * 86400 );
+       $cutoff_unixtime = $cutoff_unixtime - ($cutoff_unixtime % 86400);
+       $cutoff = $dbr->timestamp( $cutoff_unixtime );
+       if(preg_match('/^[0-9]{14}$/', $from) and $from > wfTimestamp(TS_MW,$cutoff)) {
+               $cutoff = $dbr->timestamp($from);
+       } else {
+               $from = $defaults['from'];
+       }
+
+       # 10 seconds server-side caching max
+       $wgOut->setSquidMaxage( 10 );
+
+       # Get last modified date, for client caching
+       # Don't use this if we are using the patrol feature, patrol changes don't update the timestamp
+       $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ );
+       if ( $feedFormat || !$wgUseRCPatrol ) {
+               if( $lastmod && $wgOut->checkLastModified( $lastmod ) ){
+                       # Client cache fresh and headers sent, nothing more to do.
+                       return;
+               }
+       }
+
+       # It makes no sense to hide both anons and logged-in users
+       # Where this occurs, force anons to be shown
+       $forcebot = false;
+       if( $hideanons && $hideliu ){
+               # Check if the user wants to show bots only
+               if( $hidebots ){
+                       $hideanons = 0;
+               } else {
+                       $forcebot = true;
+                       $hidebots = 0;
+               }
+       }
+
+       # Form WHERE fragments for all the options
+       $hidem  = $hideminor ? 'AND rc_minor = 0' : '';
+       $hidem .= $hidebots ? ' AND rc_bot = 0' : '';
+       $hidem .= $hideliu && !$forcebot ? ' AND rc_user = 0' : '';
+       $hidem .= ($wgUser->useRCPatrol() && $hidepatrolled ) ? ' AND rc_patrolled = 0' : '';
+       $hidem .= $hideanons && !$forcebot ? ' AND rc_user != 0' : '';
+       $hidem .= $forcebot ? ' AND rc_bot = 1' : '';
+
+       if( $hidemyself ) {
+               if( $wgUser->getId() ) {
+                       $hidem .= ' AND rc_user != ' . $wgUser->getId();
+               } else {
+                       $hidem .= ' AND rc_user_text != ' . $dbr->addQuotes( $wgUser->getName() );
+               }
+       }
+
+       // JOIN on watchlist for users
+       $uid = $wgUser->getId();
+       if( $uid ) {
+               $tables = array( 'recentchanges', 'watchlist' );
+               $join_conds = array( 'watchlist' => array('LEFT JOIN',"wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace") );
+       } else {
+               $tables = array( 'recentchanges' );
+               $join_conds = array();
+       }
+       
+       # Namespace filtering
+       $hidem .= is_null($namespace) ?  '' : ' AND rc_namespace' . ($invert ? '!=' : '=') . $namespace;
+       
+       // Is there either one namespace selected or excluded?
+       // Also, if this is "all" or main namespace, just use timestamp index.
+       if( is_null($namespace) || $invert || $namespace == NS_MAIN ) {
+               $res = $dbr->select( $tables, '*',
+                       array( "rc_timestamp >= '{$cutoff}' {$hidem}" ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, 
+                               'USE INDEX' => array('recentchanges' => 'rc_timestamp') ),
+                       $join_conds );
+       // We have a new_namespace_time index! UNION over new=(0,1) and sort result set!
+       } else {
+               // New pages
+               $sqlNew = $dbr->selectSQLText( $tables, '*',
+                       array( 'rc_new' => 1,
+                               "rc_timestamp >= '{$cutoff}' {$hidem}" ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, 
+                               'USE INDEX' =>  array('recentchanges' => 'new_name_timestamp') ),
+                       $join_conds );
+               // Old pages
+               $sqlOld = $dbr->selectSQLText( $tables, '*',
+                       array( 'rc_new' => 0,
+                               "rc_timestamp >= '{$cutoff}' {$hidem}" ),
+                       __METHOD__,
+                       array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit, 
+                               'USE INDEX' =>  array('recentchanges' => 'new_name_timestamp') ),
+                       $join_conds );
+               # Join the two fast queries, and sort the result set
+               $sql = "($sqlNew) UNION ($sqlOld) ORDER BY rc_timestamp DESC LIMIT $limit";
+               $res = $dbr->query( $sql, __METHOD__ );
+       }
+       
+       // Fetch results, prepare a batch link existence check query
+       $rows = array();
+       $batch = new LinkBatch;
+       while( $row = $dbr->fetchObject( $res ) ){
+               $rows[] = $row;
+               if ( !$feedFormat ) {
+                       // User page and talk links
+                       $batch->add( NS_USER, $row->rc_user_text  );
+                       $batch->add( NS_USER_TALK, $row->rc_user_text  );
+               }
+
+       }
+       $dbr->freeResult( $res );
+
+       if( $feedFormat ) {
+               rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod );
+       } else {
+
+               # Web output...
+
+               // Run existence checks
+               $batch->execute();
+               $any = $wgRequest->getBool( 'categories_any', $defaults['categories_any']);
+
+               // Output header
+               if ( !$specialPage->including() ) {
+                       $wgOut->addWikiText( wfMsgForContentNoTrans( "recentchangestext" ) );
+
+                       // Dump everything here
+                       $nondefaults = array();
+
+                       wfAppendToArrayIfNotDefault( 'days', $days, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'limit', $limit , $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'hideminor', $hideminor, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'hidebots', $hidebots, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'hideanons', $hideanons, $defaults, $nondefaults );
+                       wfAppendToArrayIfNotDefault( 'hideliu', $hideliu, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'hidepatrolled', $hidepatrolled, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'hidemyself', $hidemyself, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'from', $from, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'namespace', $namespace, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'invert', $invert, $defaults, $nondefaults);
+                       wfAppendToArrayIfNotDefault( 'categories_any', $any, $defaults, $nondefaults);
+
+                       // Add end of the texts
+                       $wgOut->addHTML( '<div class="rcoptions">' . rcOptionsPanel( $defaults, $nondefaults ) . "\n" );
+                       $wgOut->addHTML( rcNamespaceForm( $namespace, $invert, $nondefaults, $any ) . '</div>'."\n");
+               }
+
+               // And now for the content
+               $wgOut->setSyndicated( true );
+
+               $list = ChangesList::newFromUser( $wgUser );
+
+               if ( $wgAllowCategorizedRecentChanges ) {
+                       $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
+                       $categories = str_replace ( "|" , "\n" , $categories ) ;
+                       $categories = explode ( "\n" , $categories ) ;
+                       rcFilterByCategories ( $rows , $categories , $any ) ;
+               }
+
+               $s = $list->beginRecentChangesList();
+               $counter = 1;
+
+               $showWatcherCount = $wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' );
+               $watcherCache = array();
+
+               foreach( $rows as $obj ){
+                       if( $limit == 0) {
+                               break;
+                       }
+
+                       if ( ! ( $hideminor     && $obj->rc_minor     ) &&
+                            ! ( $hidepatrolled && $obj->rc_patrolled ) ) {
+                               $rc = RecentChange::newFromRow( $obj );
+                               $rc->counter = $counter++;
+
+                               if ($wgShowUpdatedMarker
+                                       && !empty( $obj->wl_notificationtimestamp )
+                                       && ($obj->rc_timestamp >= $obj->wl_notificationtimestamp)) {
+                                               $rc->notificationtimestamp = true;
+                               } else {
+                                       $rc->notificationtimestamp = false;
+                               }
+
+                               $rc->numberofWatchingusers = 0; // Default
+                               if ($showWatcherCount && $obj->rc_namespace >= 0) {
+                                       if (!isset($watcherCache[$obj->rc_namespace][$obj->rc_title])) {
+                                               $watcherCache[$obj->rc_namespace][$obj->rc_title] =
+                                                       $dbr->selectField( 'watchlist',
+                                                               'COUNT(*)',
+                                                               array(
+                                                                       'wl_namespace' => $obj->rc_namespace,
+                                                                       'wl_title' => $obj->rc_title,
+                                                               ),
+                                                               __METHOD__ . '-watchers' );
+                                       }
+                                       $rc->numberofWatchingusers = $watcherCache[$obj->rc_namespace][$obj->rc_title];
+                               }
+                               $s .= $list->recentChangesLine( $rc, !empty( $obj->wl_user ) );
+                               --$limit;
+                       }
+               }
+               $s .= $list->endRecentChangesList();
+               $wgOut->addHTML( $s );
+       }
+}
+
+function rcFilterByCategories ( &$rows , $categories , $any ) {
+       if( empty( $categories ) ) {
+               return;
+       }
+
+       # Filter categories
+       $cats = array () ;
+       foreach ( $categories AS $cat ) {
+               $cat = trim ( $cat ) ;
+               if ( $cat == "" ) continue ;
+               $cats[] = $cat ;
+       }
+
+       # Filter articles
+       $articles = array () ;
+       $a2r = array () ;
+       foreach ( $rows AS $k => $r ) {
+               $nt = Title::makeTitle( $r->rc_namespace, $r->rc_title );
+               $id = $nt->getArticleID() ;
+               if ( $id == 0 ) continue ; # Page might have been deleted...
+               if ( !in_array ( $id , $articles ) ) {
+                       $articles[] = $id ;
+               }
+               if ( !isset ( $a2r[$id] ) ) {
+                       $a2r[$id] = array() ;
+               }
+               $a2r[$id][] = $k ;
+       }
+
+       # Shortcut?
+       if ( count ( $articles ) == 0 OR count ( $cats ) == 0 )
+               return ;
+
+       # Look up
+       $c = new Categoryfinder ;
+       $c->seed ( $articles , $cats , $any ? "OR" : "AND" ) ;
+       $match = $c->run () ;
+
+       # Filter
+       $newrows = array () ;
+       foreach ( $match AS $id ) {
+               foreach ( $a2r[$id] AS $rev ) {
+                       $k = $rev ;
+                       $newrows[$k] = $rows[$k] ;
+               }
+       }
+       $rows = $newrows ;
+}
+
+function rcOutputFeed( $rows, $feedFormat, $limit, $hideminor, $lastmod ) {
+       global $messageMemc, $wgFeedCacheTimeout;
+       global $wgFeedClasses, $wgTitle, $wgSitename, $wgContLanguageCode;
+       global $wgFeed;
+
+       if ( !$wgFeed ) {
+               global $wgOut;
+               $wgOut->addWikiMsg( 'feed-unavailable' );
+               return;
+       }
+
+       if( !isset( $wgFeedClasses[$feedFormat] ) ) {
+               wfHttpError( 500, "Internal Server Error", "Unsupported feed type." );
+               return false;
+       }
+
+       $timekey = wfMemcKey( 'rcfeed', $feedFormat, 'timestamp' );
+       $key = wfMemcKey( 'rcfeed', $feedFormat, 'limit', $limit, 'minor', $hideminor );
+
+       $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchanges' ) .
+               ' [' . $wgContLanguageCode . ']';
+       $feed = new $wgFeedClasses[$feedFormat](
+               $feedTitle,
+               htmlspecialchars( wfMsgForContent( 'recentchanges-feed-description' ) ),
+               $wgTitle->getFullUrl() );
+
+       //purge cache if requested
+       global $wgRequest, $wgUser;
+       $purge = $wgRequest->getVal( 'action' ) == 'purge';
+       if ( $purge && $wgUser->isAllowed('purge') ) {
+               $messageMemc->delete( $timekey );
+               $messageMemc->delete( $key );
+       }
+
+       /**
+        * Bumping around loading up diffs can be pretty slow, so where
+        * possible we want to cache the feed output so the next visitor
+        * gets it quick too.
+        */
+       $cachedFeed = false;
+       if( ( $wgFeedCacheTimeout > 0 ) && ( $feedLastmod = $messageMemc->get( $timekey ) ) ) {
+               /**
+                * If the cached feed was rendered very recently, we may
+                * go ahead and use it even if there have been edits made
+                * since it was rendered. This keeps a swarm of requests
+                * from being too bad on a super-frequently edited wiki.
+                */
+               if( time() - wfTimestamp( TS_UNIX, $feedLastmod )
+                               < $wgFeedCacheTimeout
+                       || wfTimestamp( TS_UNIX, $feedLastmod )
+                               > wfTimestamp( TS_UNIX, $lastmod ) ) {
+                       wfDebug( "RC: loading feed from cache ($key; $feedLastmod; $lastmod)...\n" );
+                       $cachedFeed = $messageMemc->get( $key );
+               } else {
+                       wfDebug( "RC: cached feed timestamp check failed ($feedLastmod; $lastmod)\n" );
+               }
+       }
+       if( is_string( $cachedFeed ) ) {
+               wfDebug( "RC: Outputting cached feed\n" );
+               $feed->httpHeaders();
+               echo $cachedFeed;
+       } else {
+               wfDebug( "RC: rendering new feed and caching it\n" );
+               ob_start();
+               rcDoOutputFeed( $rows, $feed );
+               $cachedFeed = ob_get_contents();
+               ob_end_flush();
+
+               $expire = 3600 * 24; # One day
+               $messageMemc->set( $key, $cachedFeed );
+               $messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
+       }
+       return true;
+}
+
+/**
+ * @todo document
+ * @param $rows Database resource with recentchanges rows
+ */
+function rcDoOutputFeed( $rows, &$feed ) {
+       wfProfileIn( __METHOD__ );
+
+       $feed->outHeader();
+
+       # Merge adjacent edits by one user
+       $sorted = array();
+       $n = 0;
+       foreach( $rows as $obj ) {
+               if( $n > 0 &&
+                       $obj->rc_namespace >= 0 &&
+                       $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id &&
+                       $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) {
+                       $sorted[$n-1]->rc_last_oldid = $obj->rc_last_oldid;
+               } else {
+                       $sorted[$n] = $obj;
+                       $n++;
+               }
+       }
+
+       foreach( $sorted as $obj ) {
+               $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
+               $talkpage = $title->getTalkPage();
+               $item = new FeedItem(
+                       $title->getPrefixedText(),
+                       rcFormatDiff( $obj ),
+                       $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ),
+                       $obj->rc_timestamp,
+                       ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text,
+                       $talkpage->getFullURL()
+                       );
+               $feed->outItem( $item );
+       }
+       $feed->outFooter();
+       wfProfileOut( __METHOD__ );
+}
+
+/**
+ *
+ */
+function rcCountLink( $lim, $d, $page='Recentchanges', $more='', $active = false ) {
+       global $wgUser, $wgLang, $wgContLang;
+       $sk = $wgUser->getSkin();
+       $s = $sk->makeKnownLink( $wgContLang->specialPage( $page ),
+         ($lim ? $wgLang->formatNum( "{$lim}" ) : wfMsg( 'recentchangesall' ) ), "{$more}" .
+         ($d ? "days={$d}&" : '') . 'limit='.$lim, '', '',
+         $active ? 'style="font-weight: bold;"' : '' );
+       return $s;
+}
+
+/**
+ *
+ */
+function rcDaysLink( $lim, $d, $page='Recentchanges', $more='', $active = false ) {
+       global $wgUser, $wgLang, $wgContLang;
+       $sk = $wgUser->getSkin();
+       $s = $sk->makeKnownLink( $wgContLang->specialPage( $page ),
+         ($d ? $wgLang->formatNum( "{$d}" ) : wfMsg( 'recentchangesall' ) ), $more.'days='.$d .
+         ($lim ? '&limit='.$lim : ''), '', '',
+         $active ? 'style="font-weight: bold;"' : '' );
+       return $s;
+}
+
+/**
+ * Used by Recentchangeslinked
+ */
+function rcDayLimitLinks( $days, $limit, $page='Recentchanges', $more='', $doall = false, $minorLink = '',
+       $botLink = '', $liuLink = '', $patrLink = '', $myselfLink = '' ) {
+       global $wgRCLinkLimits, $wgRCLinkDays;
+       if ($more != '') $more .= '&';
+       
+       # Sort data for display and make sure it's unique after we've added user data.
+       # FIXME: why does this piss around with globals like this? Why is $limit added on globally?
+       $wgRCLinkLimits[] = $limit;
+       $wgRCLinkDays[] = $days;
+       sort($wgRCLinkLimits);
+       sort($wgRCLinkDays);
+       $wgRCLinkLimits = array_unique($wgRCLinkLimits);
+       $wgRCLinkDays = array_unique($wgRCLinkDays);
+       
+       $cl = array();
+       foreach( $wgRCLinkLimits as $countLink ) {
+               $cl[] = rcCountLink( $countLink, $days, $page, $more, $countLink == $limit );
+       }
+       if( $doall ) $cl[] = rcCountLink( 0, $days, $page, $more );
+       $cl = implode( ' | ', $cl);
+       
+       $dl = array();
+       foreach( $wgRCLinkDays as $daysLink ) {
+               $dl[] = rcDaysLink( $limit, $daysLink, $page, $more, $daysLink == $days );
+       }
+       if( $doall ) $dl[] = rcDaysLink( $limit, 0, $page, $more );
+       $dl = implode( ' | ', $dl);
+       
+       $linkParts = array( 'minorLink' => 'minor', 'botLink' => 'bots', 'liuLink' => 'liu', 'patrLink' => 'patr', 'myselfLink' => 'mine' );
+       foreach( $linkParts as $linkVar => $linkMsg ) {
+               if( $$linkVar != '' )
+                       $links[] = wfMsgHtml( 'rcshowhide' . $linkMsg, $$linkVar );
+       }
+
+       $shm = implode( ' | ', $links );
+       $note = wfMsg( 'rclinks', $cl, $dl, $shm );
+       return $note;
+}
+
+
+/**
+ * Makes change an option link which carries all the other options
+ * @param $title see Title
+ * @param $override
+ * @param $options
+ */
+function makeOptionsLink( $title, $override, $options, $active = false ) {
+       global $wgUser, $wgContLang;
+       $sk = $wgUser->getSkin();
+       return $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchanges' ),
+               htmlspecialchars( $title ), wfArrayToCGI( $override, $options ), '', '',
+               $active ? 'style="font-weight: bold;"' : '' );
+}
+
+/**
+ * Creates the options panel.
+ * @param $defaults
+ * @param $nondefaults
+ */
+function rcOptionsPanel( $defaults, $nondefaults ) {
+       global $wgLang, $wgUser, $wgRCLinkLimits, $wgRCLinkDays;
+
+       $options = $nondefaults + $defaults;
+
+       if( $options['from'] )
+               $note = wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
+                       $wgLang->formatNum( $options['limit'] ),
+                       $wgLang->timeanddate( $options['from'], true ) );
+       else
+               $note = wfMsgExt( 'rcnote', array( 'parseinline' ),
+                       $wgLang->formatNum( $options['limit'] ),
+                       $wgLang->formatNum( $options['days'] ),
+                       $wgLang->timeAndDate( wfTimestampNow(), true ) );
+
+       # Sort data for display and make sure it's unique after we've added user data.
+       $wgRCLinkLimits[] = $options['limit'];
+       $wgRCLinkDays[] = $options['days'];
+       sort($wgRCLinkLimits);
+       sort($wgRCLinkDays);
+       $wgRCLinkLimits = array_unique($wgRCLinkLimits);
+       $wgRCLinkDays = array_unique($wgRCLinkDays);
+       
+       // limit links
+       foreach( $wgRCLinkLimits as $value ) {
+               $cl[] = makeOptionsLink( $wgLang->formatNum( $value ),
+                       array( 'limit' => $value ), $nondefaults, $value == $options['limit'] ) ;
+       }
+       $cl = implode( ' | ', $cl);
+
+       // day links, reset 'from' to none
+       foreach( $wgRCLinkDays as $value ) {
+               $dl[] = makeOptionsLink( $wgLang->formatNum( $value ),
+                       array( 'days' => $value, 'from' => '' ), $nondefaults, $value == $options['days'] ) ;
+       }
+       $dl = implode( ' | ', $dl);
+
+
+       // show/hide links
+       $showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ));
+       $minorLink = makeOptionsLink( $showhide[1-$options['hideminor']],
+               array( 'hideminor' => 1-$options['hideminor'] ), $nondefaults);
+       $botLink = makeOptionsLink( $showhide[1-$options['hidebots']],
+               array( 'hidebots' => 1-$options['hidebots'] ), $nondefaults);
+       $anonsLink = makeOptionsLink( $showhide[ 1 - $options['hideanons'] ],
+               array( 'hideanons' => 1 - $options['hideanons'] ), $nondefaults );
+       $liuLink   = makeOptionsLink( $showhide[1-$options['hideliu']],
+               array( 'hideliu' => 1-$options['hideliu'] ), $nondefaults);
+       $patrLink  = makeOptionsLink( $showhide[1-$options['hidepatrolled']],
+               array( 'hidepatrolled' => 1-$options['hidepatrolled'] ), $nondefaults);
+       $myselfLink = makeOptionsLink( $showhide[1-$options['hidemyself']],
+               array( 'hidemyself' => 1-$options['hidemyself'] ), $nondefaults);
+
+       $links[] = wfMsgHtml( 'rcshowhideminor', $minorLink );
+       $links[] = wfMsgHtml( 'rcshowhidebots', $botLink );
+       $links[] = wfMsgHtml( 'rcshowhideanons', $anonsLink );
+       $links[] = wfMsgHtml( 'rcshowhideliu', $liuLink );
+       if( $wgUser->useRCPatrol() )
+               $links[] = wfMsgHtml( 'rcshowhidepatr', $patrLink );
+       $links[] = wfMsgHtml( 'rcshowhidemine', $myselfLink );
+       $hl = implode( ' | ', $links );
+
+       // show from this onward link
+       $now = $wgLang->timeanddate( wfTimestampNow(), true );
+       $tl =  makeOptionsLink( $now, array( 'from' => wfTimestampNow()), $nondefaults );
+
+       $rclinks = wfMsgExt( 'rclinks', array( 'parseinline', 'replaceafter'),
+               $cl, $dl, $hl );
+       $rclistfrom = wfMsgExt( 'rclistfrom', array( 'parseinline', 'replaceafter'), $tl );
+       return "$note<br />$rclinks<br />$rclistfrom";
+
+}
+
+/**
+ * Creates the choose namespace selection
+ *
+ * @private
+ *
+ * @param $namespace Mixed: the key of the currently selected namespace, empty string
+ *              if there is none
+ * @param $invert Bool: whether to invert the namespace selection
+ * @param $nondefaults Array: an array of non default options to be remembered
+ * @param $categories_any Bool: Default value for the checkbox
+ *
+ * @return string
+ */
+function rcNamespaceForm( $namespace, $invert, $nondefaults, $categories_any ) {
+       global $wgScript, $wgAllowCategorizedRecentChanges, $wgRequest;
+       $t = SpecialPage::getTitleFor( 'Recentchanges' );
+
+       $namespaceselect = HTMLnamespaceselector($namespace, '');
+       $submitbutton = '<input type="submit" value="' . wfMsgHtml( 'allpagessubmit' ) . "\" />\n";
+       $invertbox = "<input type='checkbox' name='invert' value='1' id='nsinvert'" . ( $invert ? ' checked="checked"' : '' ) . ' />';
+
+       if ( $wgAllowCategorizedRecentChanges ) {
+               $categories = trim ( $wgRequest->getVal ( 'categories' , "" ) ) ;
+               $cb_arr = array( 'type' => 'checkbox', 'name' => 'categories_any', 'value' => "1" ) ;
+               if ( $categories_any ) $cb_arr['checked'] = "checked" ;
+               $catbox = "<br />" ;
+               $catbox .= wfMsgExt('rc_categories', array('parseinline')) . " ";
+               $catbox .= wfElement('input', array( 'type' => 'text', 'name' => 'categories', 'value' => $categories));
+               $catbox .= " &nbsp;" ;
+               $catbox .= wfElement('input', $cb_arr );
+               $catbox .= wfMsgExt('rc_categories_any', array('parseinline'));
+       } else {
+               $catbox = "" ;
+       }
+
+       $out = "<div class='namespacesettings'><form method='get' action='{$wgScript}'>\n";
+
+       foreach ( $nondefaults as $key => $value ) {
+               if ($key != 'namespace' && $key != 'invert')
+                       $out .= wfElement('input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value));
+       }
+
+       $out .= '<input type="hidden" name="title" value="'.$t->getPrefixedText().'" />';
+       $out .= "
+<div id='nsselect' class='recentchanges'>
+       <label for='namespace'>" . wfMsgHtml('namespace') . "</label>
+       {$namespaceselect}{$submitbutton}{$invertbox} <label for='nsinvert'>" . wfMsgHtml('invert') . "</label>{$catbox}\n</div>";
+       $out .= '</form></div>';
+       return $out;
+}
+
+
+/**
+ * Format a diff for the newsfeed
+ */
+function rcFormatDiff( $row ) {
+       global $wgUser;
+
+       $titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+       $timestamp = wfTimestamp( TS_MW, $row->rc_timestamp );
+       $actiontext = '';
+       if( $row->rc_type == RC_LOG ) {
+               if( $row->rc_deleted & LogPage::DELETED_ACTION ) {
+                       $actiontext = wfMsgHtml('rev-deleted-event');
+               } else {
+                       $actiontext = LogPage::actionText( $row->rc_log_type, $row->rc_log_action,
+                               $titleObj, $wgUser->getSkin(), LogPage::extractParams($row->rc_params,true,true) );
+               }
+       }
+       return rcFormatDiffRow( $titleObj,
+               $row->rc_last_oldid, $row->rc_this_oldid,
+               $timestamp,
+               ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMsgHtml('rev-deleted-comment') : $row->rc_comment,
+               $actiontext );
+}
+
+function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
+       global $wgFeedDiffCutoff, $wgContLang, $wgUser;
+       wfProfileIn( __FUNCTION__ );
+
+       $skin = $wgUser->getSkin();
+       # log enties
+       if( $actiontext ) {
+               $comment = "$actiontext $comment";
+       }
+       $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n";
+
+       //NOTE: Check permissions for anonymous users, not current user.
+       //      No "privileged" version should end up in the cache.
+       //      Most feed readers will not log in anway.
+       $anon = new User();
+       $accErrors = $title->getUserPermissionsErrors( 'read', $anon, true );
+
+       if( $title->getNamespace() >= 0 && !$accErrors ) {
+               if( $oldid ) {
+                       wfProfileIn( __FUNCTION__."-dodiff" );
+
+                       $de = new DifferenceEngine( $title, $oldid, $newid );
+                       #$diffText = $de->getDiff( wfMsg( 'revisionasof',
+                       #       $wgContLang->timeanddate( $timestamp ) ),
+                       #       wfMsg( 'currentrev' ) );
+                       $diffText = $de->getDiff(
+                               wfMsg( 'previousrevision' ), // hack
+                               wfMsg( 'revisionasof',
+                                       $wgContLang->timeanddate( $timestamp ) ) );
+
+
+                       if ( strlen( $diffText ) > $wgFeedDiffCutoff ) {
+                               // Omit large diffs
+                               $diffLink = $title->escapeFullUrl(
+                                       'diff=' . $newid .
+                                       '&oldid=' . $oldid );
+                               $diffText = '<a href="' .
+                                       $diffLink .
+                                       '">' .
+                                       htmlspecialchars( wfMsgForContent( 'difference' ) ) .
+                                       '</a>';
+                       } elseif ( $diffText === false ) {
+                               // Error in diff engine, probably a missing revision
+                               $diffText = "<p>Can't load revision $newid</p>";
+                       } else {
+                               // Diff output fine, clean up any illegal UTF-8
+                               $diffText = UtfNormal::cleanUp( $diffText );
+                               $diffText = rcApplyDiffStyle( $diffText );
+                       }
+                       wfProfileOut( __FUNCTION__."-dodiff" );
+               } else {
+                       $rev = Revision::newFromId( $newid );
+                       if( is_null( $rev ) ) {
+                               $newtext = '';
+                       } else {
+                               $newtext = $rev->getText();
+                       }
+                       $diffText = '<p><b>' . wfMsg( 'newpage' ) . '</b></p>' .
+                               '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
+               }
+               $completeText .= $diffText;
+       }
+
+       wfProfileOut( __FUNCTION__ );
+       return $completeText;
+}
+
+/**
+ * Hacky application of diff styles for the feeds.
+ * Might be 'cleaner' to use DOM or XSLT or something,
+ * but *gack* it's a pain in the ass.
+ *
+ * @param $text String:
+ * @return string
+ * @private
+ */
+function rcApplyDiffStyle( $text ) {
+       $styles = array(
+               'diff'             => 'background-color: white; color:black;',
+               'diff-otitle'      => 'background-color: white; color:black;',
+               'diff-ntitle'      => 'background-color: white; color:black;',
+               'diff-addedline'   => 'background: #cfc; color:black; font-size: smaller;',
+               'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;',
+               'diff-context'     => 'background: #eee; color:black; font-size: smaller;',
+               'diffchange'       => 'color: red; font-weight: bold; text-decoration: none;',
+       );
+
+       foreach( $styles as $class => $style ) {
+               $text = preg_replace( "/(<[^>]+)class=(['\"])$class\\2([^>]*>)/",
+                       "\\1style=\"$style\"\\3", $text );
+       }
+
+       return $text;
+}
diff --git a/includes/specials/Recentchangeslinked.php b/includes/specials/Recentchangeslinked.php
new file mode 100644 (file)
index 0000000..12a1a23
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+/**
+ * This is to display changes made to all articles linked in an article.
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+require_once( 'SpecialRecentchanges.php' );
+
+/**
+ * Entrypoint
+ * @param string $par parent page we will look at
+ */
+function wfSpecialRecentchangeslinked( $par = NULL ) {
+       global $wgUser, $wgOut, $wgLang, $wgContLang, $wgRequest, $wgTitle, $wgScript;
+
+       $days = $wgRequest->getInt( 'days' );
+       $target = isset($par) ? $par : $wgRequest->getVal( 'target' );
+       $hideminor = $wgRequest->getBool( 'hideminor' ) ? 1 : 0;
+       $showlinkedto = $wgRequest->getBool( 'showlinkedto' ) ? 1 : 0;
+
+       $title = Title::newFromURL( $target );
+       $target = $title ? $title->getPrefixedText() : '';
+
+       $wgOut->setPagetitle( wfMsg( 'recentchangeslinked' ) );
+       $sk = $wgUser->getSkin();
+
+       $wgOut->addHTML(
+               Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+               Xml::openElement( 'fieldset' ) .
+               Xml::element( 'legend', array(), wfMsg( 'recentchangeslinked' ) ) . "\n" .
+               Xml::inputLabel( wfMsg( 'recentchangeslinked-page' ), 'target', 'recentchangeslinked-target', 40, $target ) .
+               "&nbsp;&nbsp;&nbsp;<span style='white-space: nowrap'>" .
+               Xml::check( 'showlinkedto', $showlinkedto, array('id' => 'showlinkedto') ) . ' ' .
+               Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) .
+               "</span><br/>\n" .
+               Xml::hidden( 'title', $wgTitle->getPrefixedText() ). "\n" .
+               Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+               Xml::closeElement( 'fieldset' ) .
+               Xml::closeElement( 'form' ) . "\n"
+       );
+
+       if ( !$target ) {
+               return;
+       }
+       $nt = Title::newFromURL( $target );
+       if( !$nt ) {
+               $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+               return;
+       }
+       $id = $nt->getArticleId();
+
+       $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) );
+       $wgOut->setSyndicated();
+       $wgOut->setFeedAppendQuery( "target=" . urlencode( $target ) );
+
+       if ( !$days ) {
+               $days = (int)$wgUser->getOption( 'rcdays', 7 );
+       }
+       list( $limit, /* offset */ ) = wfCheckLimits( 100, 'rclimit' );
+
+       $dbr = wfGetDB( DB_SLAVE,'recentchangeslinked' );
+       $cutoff = $dbr->timestamp( time() - ( $days * 86400 ) );
+
+       $hideminor = ($hideminor ? 1 : 0);
+       if ( $hideminor ) {
+               $mlink = $sk->makeKnownLink( $wgContLang->specialPage( 'Recentchangeslinked' ),
+                 wfMsg( 'show' ), 'target=' . htmlspecialchars( $nt->getPrefixedURL() ) .
+                 "&days={$days}&limit={$limit}&hideminor=0&showlinkedto={$showlinkedto}" );
+       } else {
+               $mlink = $sk->makeKnownLink( $wgContLang->specialPage( "Recentchangeslinked" ),
+                 wfMsg( "hide" ), "target=" . htmlspecialchars( $nt->getPrefixedURL() ) .
+                 "&days={$days}&limit={$limit}&hideminor=1&showlinkedto={$showlinkedto}" );
+       }
+       if ( $hideminor ) {
+               $cmq = 'AND rc_minor=0';
+       } else { $cmq = ''; }
+
+       list($recentchanges, $categorylinks, $pagelinks, $watchlist) =
+           $dbr->tableNamesN( 'recentchanges', 'categorylinks', 'pagelinks', "watchlist" );
+
+       $uid = $wgUser->getId();
+       // The fields we are selecting
+       $fields = "rc_cur_id,rc_namespace,rc_title,
+               rc_user,rc_comment,rc_user_text,rc_timestamp,rc_minor,rc_log_type,rc_log_action,rc_params,rc_deleted,
+               rc_new, rc_id, rc_this_oldid, rc_last_oldid, rc_bot, rc_patrolled, rc_type, rc_old_len, rc_new_len";
+       $fields .= $uid ? ",wl_user" : "";
+
+       // Check if this should be a feed
+       $feed = false;
+       global $wgSitename, $wgFeedClasses, $wgContLanguageCode, $wgFeedLimit;
+       $feedFormat = $wgRequest->getVal( 'feed' );
+       if( $feedFormat && isset( $wgFeedClasses[$feedFormat] ) ) {
+               $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'recentchangeslinked-title', $nt->getPrefixedText() ) . 
+                       ' [' . $wgContLanguageCode . ']';
+               $feed = new $wgFeedClasses[$feedFormat]( $feedTitle, 
+                       htmlspecialchars( wfMsgForContent('recentchangeslinked') ), $wgTitle->getFullUrl() );
+               # Sanity check
+               if( $limit > $wgFeedLimit ) {
+                       $limit = $wgFeedLimit;
+               }
+       }
+
+       // If target is a Category, use categorylinks and invert from and to
+       if( $nt->getNamespace() == NS_CATEGORY ) {
+               $catkey = $dbr->addQuotes( $nt->getDBkey() );
+               # The table clauses
+               $tables = "$categorylinks, $recentchanges";
+               $tables .= $uid ? " LEFT JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "";
+
+               $sql = "SELECT /* wfSpecialRecentchangeslinked */ $fields FROM $tables 
+                       WHERE rc_timestamp > '{$cutoff}' {$cmq} 
+                       AND cl_from=rc_cur_id 
+                       AND cl_to=$catkey 
+                       GROUP BY $fields ORDER BY rc_timestamp DESC LIMIT {$limit}"; // Shitty-ass GROUP BY by for postgres apparently
+       } else {
+               if( $showlinkedto ) {
+                       $ns = $dbr->addQuotes( $nt->getNamespace() );
+                       $dbkey = $dbr->addQuotes( $nt->getDBkey() );
+                       $joinConds = "AND pl_namespace={$ns} AND pl_title={$dbkey} AND pl_from=rc_cur_id";
+               } else {
+                       $joinConds = "AND pl_namespace=rc_namespace AND pl_title=rc_title AND pl_from=$id";
+               }
+               # The table clauses
+               $tables = "$pagelinks, $recentchanges";
+               $tables .= $uid ? " LEFT JOIN $watchlist ON wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace " : "";
+
+               $sql = "SELECT /* wfSpecialRecentchangeslinked */ $fields FROM $tables
+                       WHERE rc_timestamp > '{$cutoff}' {$cmq}
+                       {$joinConds}
+                       GROUP BY $fields ORDER BY rc_timestamp DESC LIMIT {$limit}"; // Shitty-ass GROUP BY by for postgres apparently
+       }
+       # Actually do the query
+       $res = $dbr->query( $sql, __METHOD__ );
+       $count = $dbr->numRows( $res );
+       $rchanges = array();
+       # Output feeds now and be done with it!
+       if( $feed ) {
+               if( $count ) {
+                       $counter = 1;
+                       while ( $limit ) {
+                               if ( 0 == $count ) { break; }
+                               $obj = $dbr->fetchObject( $res );
+                               --$count;
+                               $rc = RecentChange::newFromRow( $obj );
+                               $rc->counter = $counter++;
+                               --$limit;
+                               $rchanges[] = $obj;
+                       }
+               }
+               require_once( "SpecialRecentchanges.php" );
+               $wgOut->disable();
+               rcDoOutputFeed( $rchanges, $feed );
+               return;
+       }
+       
+       # Otherwise, carry on with regular output...
+       $wgOut->addHTML("&lt; ".$sk->makeLinkObj($nt, "", "redirect=no" )."<br />\n");
+       $note = wfMsgExt( "rcnote", array ( 'parseinline' ), $limit, $days, $wgLang->timeAndDate( wfTimestampNow(), true ) );
+       $wgOut->addHTML( "<hr />\n{$note}\n<br />" );
+
+       $note = rcDayLimitlinks( $days, $limit, "Recentchangeslinked",
+                                "target=" . $nt->getPrefixedURL() . "&hideminor={$hideminor}&showlinkedto={$showlinkedto}",
+                                false, $mlink );
+
+       $wgOut->addHTML( $note."\n" );
+
+       $list = ChangesList::newFromUser( $wgUser );
+       $s = $list->beginRecentChangesList();
+
+       if ( $count ) {
+               $counter = 1;
+               while ( $limit ) {
+                       if ( 0 == $count ) { break; }
+                       $obj = $dbr->fetchObject( $res );
+                       --$count;
+                       $rc = RecentChange::newFromRow( $obj );
+                       $rc->counter = $counter++;
+                       $s .= $list->recentChangesLine( $rc , !empty( $obj->wl_user) );
+                       --$limit;
+               }
+       } else {
+               $wgOut->addWikiMsg('recentchangeslinked-noresult');
+       }
+       $s .= $list->endRecentChangesList();
+
+       $dbr->freeResult( $res );
+       $wgOut->addHTML( $s );
+}
diff --git a/includes/specials/Resetpass.php b/includes/specials/Resetpass.php
new file mode 100644 (file)
index 0000000..707b941
--- /dev/null
@@ -0,0 +1,167 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/** Constructor */
+function wfSpecialResetpass( $par ) {
+       $form = new PasswordResetForm();
+       $form->execute( $par );
+}
+
+/**
+ * Let users recover their password.
+ * @ingroup SpecialPage
+ */
+class PasswordResetForm extends SpecialPage {
+       function __construct( $name=null, $reset=null ) {
+               if( $name !== null ) {
+                       $this->mName = $name;
+                       $this->mTemporaryPassword = $reset;
+               } else {
+                       global $wgRequest;
+                       $this->mName = $wgRequest->getVal( 'wpName' );
+                       $this->mTemporaryPassword = $wgRequest->getVal( 'wpPassword' );
+               }
+       }
+
+       /**
+        * Main execution point
+        */
+       function execute( $par ) {
+               global $wgUser, $wgAuth, $wgOut, $wgRequest;
+
+               if( !$wgAuth->allowPasswordChange() ) {
+                       $this->error( wfMsg( 'resetpass_forbidden' ) );
+                       return;
+               }
+
+               if( $this->mName === null && !$wgRequest->wasPosted() ) {
+                       $this->error( wfMsg( 'resetpass_missing' ) );
+                       return;
+               }
+
+               if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
+                       $newpass = $wgRequest->getVal( 'wpNewPassword' );
+                       $retype = $wgRequest->getVal( 'wpRetype' );
+                       try {
+                               $this->attemptReset( $newpass, $retype );
+                               $wgOut->addWikiMsg( 'resetpass_success' );
+
+                               $data = array(
+                                       'action' => 'submitlogin',
+                                       'wpName' => $this->mName,
+                                       'wpPassword' => $newpass,
+                                       'returnto' => $wgRequest->getVal( 'returnto' ),
+                               );
+                               if( $wgRequest->getCheck( 'wpRemember' ) ) {
+                                       $data['wpRemember'] = 1;
+                               }
+                               $login = new LoginForm( new FauxRequest( $data, true ) );
+                               $login->execute();
+
+                               return;
+                       } catch( PasswordError $e ) {
+                               $this->error( $e->getMessage() );
+                       }
+               }
+               $this->showForm();
+       }
+
+       function error( $msg ) {
+               global $wgOut;
+               $wgOut->addHtml( '<div class="errorbox">' .
+                       htmlspecialchars( $msg ) .
+                       '</div>' );
+       }
+
+       function showForm() {
+               global $wgOut, $wgUser, $wgRequest;
+
+               $wgOut->disallowUserJs();
+
+               $self = SpecialPage::getTitleFor( 'Resetpass' );
+               $form  =
+                       '<div id="userloginForm">' .
+                       wfOpenElement( 'form',
+                               array(
+                                       'method' => 'post',
+                                       'action' => $self->getLocalUrl() ) ) .
+                       '<h2>' . wfMsgHtml( 'resetpass_header' ) . '</h2>' .
+                       '<div id="userloginprompt">' .
+                       wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
+                       '</div>' .
+                       '<table>' .
+                       wfHidden( 'token', $wgUser->editToken() ) .
+                       wfHidden( 'wpName', $this->mName ) .
+                       wfHidden( 'wpPassword', $this->mTemporaryPassword ) .
+                       wfHidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
+                       $this->pretty( array(
+                               array( 'wpName', 'username', 'text', $this->mName ),
+                               array( 'wpNewPassword', 'newpassword', 'password', '' ),
+                               array( 'wpRetype', 'yourpasswordagain', 'password', '' ),
+                       ) ) .
+                       '<tr>' .
+                               '<td></td>' .
+                               '<td>' .
+                                       Xml::checkLabel( wfMsg( 'remembermypassword' ),
+                                               'wpRemember', 'wpRemember',
+                                               $wgRequest->getCheck( 'wpRemember' ) ) .
+                               '</td>' .
+                       '</tr>' .
+                       '<tr>' .
+                               '<td></td>' .
+                               '<td>' .
+                                       wfSubmitButton( wfMsgHtml( 'resetpass_submit' ) ) .
+                               '</td>' .
+                       '</tr>' .
+                       '</table>' .
+                       wfCloseElement( 'form' ) .
+                       '</div>';
+               $wgOut->addHtml( $form );
+       }
+
+       function pretty( $fields ) {
+               $out = '';
+               foreach( $fields as $list ) {
+                       list( $name, $label, $type, $value ) = $list;
+                       if( $type == 'text' ) {
+                               $field = '<tt>' . htmlspecialchars( $value ) . '</tt>';
+                       } else {
+                               $field = Xml::input( $name, 20, $value,
+                                       array( 'id' => $name, 'type' => $type ) );
+                       }
+                       $out .= '<tr>';
+                       $out .= '<td align="right">';
+                       $out .= Xml::label( wfMsg( $label ), $name );
+                       $out .= '</td>';
+                       $out .= '<td>';
+                       $out .= $field;
+                       $out .= '</td>';
+                       $out .= '</tr>';
+               }
+               return $out;
+       }
+
+       /**
+        * @throws PasswordError when cannot set the new password because requirements not met.
+        */
+       function attemptReset( $newpass, $retype ) {
+               $user = User::newFromName( $this->mName );
+               if( $user->isAnon() ) {
+                       throw new PasswordError( 'no such user' );
+               }
+
+               if( !$user->checkTemporaryPassword( $this->mTemporaryPassword ) ) {
+                       throw new PasswordError( wfMsg( 'resetpass_bad_temporary' ) );
+               }
+
+               if( $newpass !== $retype ) {
+                       throw new PasswordError( wfMsg( 'badretype' ) );
+               }
+
+               $user->setPassword( $newpass );
+               $user->saveSettings();
+       }
+}
diff --git a/includes/specials/Revisiondelete.php b/includes/specials/Revisiondelete.php
new file mode 100644 (file)
index 0000000..a3d4b7e
--- /dev/null
@@ -0,0 +1,1470 @@
+<?php
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and hide revisions. Log items can also be hidden.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+function wfSpecialRevisiondelete( $par = null ) {
+       global $wgOut, $wgRequest, $wgUser;
+       # Handle our many different possible input types
+       $target = $wgRequest->getText( 'target' );
+       $oldid = $wgRequest->getArray( 'oldid' );
+       $artimestamp = $wgRequest->getArray( 'artimestamp' );
+       $logid = $wgRequest->getArray( 'logid' );
+       $img = $wgRequest->getArray( 'oldimage' );
+       $fileid = $wgRequest->getArray( 'fileid' );
+       # For reviewing deleted files...
+       $file = $wgRequest->getVal( 'file' );
+       # If this is a revision, then we need a target page
+       $page = Title::newFromUrl( $target );
+       if( is_null($page) ) {
+               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
+               return;
+       }
+       # Only one target set at a time please!
+       $i = (bool)$file + (bool)$oldid + (bool)$logid + (bool)$artimestamp + (bool)$fileid + (bool)$img;
+       if( $i !== 1 ) {
+               $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+               return;
+       }
+       # Logs must have a type given
+       if( $logid && !strpos($page->getDBKey(),'/') ) {
+               $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+               return;
+       }
+       # Either submit or create our form
+       $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $img, $file );
+       if( $wgRequest->wasPosted() ) {
+               $form->submit( $wgRequest );
+       } else if( $oldid || $artimestamp ) {
+               $form->showRevs();
+       } else if( $fileid || $img ) {
+               $form->showImages();
+       } else if( $logid ) {
+               $form->showLogItems();
+       }
+       # Show relevant lines from the deletion log. This will show even if said ID
+       # does not exist...might be helpful
+       $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+       LogEventsList::showLogExtract( $wgOut, 'delete', $page->getPrefixedText() );
+       if( $wgUser->isAllowed( 'suppressionlog' ) ){
+               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
+               LogEventsList::showLogExtract( $wgOut, 'suppress', $page->getPrefixedText() );
+       }
+}
+
+/**
+ * Implements the GUI for Revision Deletion.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleteForm {
+       /**
+        * @param Title $page
+        * @param array $oldids
+        * @param array $logids
+        * @param array $artimestamps
+        * @param array $fileids
+        * @param array $img
+        * @param string $file
+        */
+       function __construct( $page, $oldids, $logids, $artimestamps, $fileids, $img, $file ) {
+               global $wgUser, $wgOut;
+
+               $this->page = $page;
+               # For reviewing deleted files...
+               if( $file ) {
+                       $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file );
+                       $oimage->load();
+                       // Check if user is allowed to see this file
+                       if( !$oimage->userCan(File::DELETED_FILE) ) {
+                               $wgOut->permissionRequired( 'suppressrevision' );
+                       } else {
+                               $this->showFile( $file );
+                       }
+                       return;
+               }
+               $this->skin = $wgUser->getSkin();
+               # Give a link to the log for this page
+               if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
+                       $links = array();
+
+                       $logtitle = SpecialPage::getTitleFor( 'Log' );
+                       $links[] = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
+                               wfArrayToCGI( array( 'page' => $this->page->getPrefixedUrl() ) ) );
+                       # Give a link to the page history
+                       $links[] = $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml( 'pagehist' ),
+                               wfArrayToCGI( array( 'action' => 'history' ) ) );
+                       # Link to deleted edits
+                       if( $wgUser->isAllowed('undelete') ) {
+                               $undelete = SpecialPage::getTitleFor( 'Undelete' );
+                               $links[] = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'deletedhist' ),
+                                       wfArrayToCGI( array( 'target' => $this->page->getPrefixedUrl() ) ) );
+                       }
+                       # Logs themselves don't have histories or archived revisions
+                       $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
+               }
+               // At this point, we should only have one of these
+               if( $oldids ) {
+                       $this->revisions = $oldids;
+                       $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+                       $this->deleteKey='oldid';
+               } else if( $artimestamps ) {
+                       $this->archrevs = $artimestamps;
+                       $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+                       $this->deleteKey='artimestamp';
+               } else if( $img ) {
+                       $this->ofiles = $img;
+                       $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+                       $this->deleteKey='oldimage';
+               } else if( $fileids ) {
+                       $this->afiles = $fileids;
+                       $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+                       $this->deleteKey='fileid';
+               } else if( $logids ) {
+                       $this->events = $logids;
+                       $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogPage::DELETED_ACTION );
+                       $this->deleteKey='logid';
+               }
+               // Our checkbox messages depends one what we are doing,
+               // e.g. we don't hide "text" for logs or images
+               $this->checks = array(
+                       $hide_content_name,
+                       array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
+                       array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ) );
+               if( $wgUser->isAllowed('suppressrevision') ) {
+                       $this->checks[] = array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED );
+               }
+       }
+
+       /**
+        * Show a deleted file version requested by the visitor.
+        */
+       private function showFile( $key ) {
+               global $wgOut, $wgRequest;
+               $wgOut->disable();
+
+               # We mustn't allow the output to be Squid cached, otherwise
+               # if an admin previews a deleted image, and it's cached, then
+               # a user without appropriate permissions can toddle off and
+               # nab the image, and Squid will serve it
+               $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+               $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
+               $wgRequest->response()->header( 'Pragma: no-cache' );
+
+               $store = FileStore::get( 'deleted' );
+               $store->stream( $key );
+       }
+
+       /**
+        * This lets a user set restrictions for live and archived revisions
+        */
+       function showRevs() {
+               global $wgOut, $wgUser, $action;
+
+               $UserAllowed = true;
+
+               $count = ($this->deleteKey=='oldid') ?
+                       count($this->revisions) : count($this->archrevs);
+               $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
+
+               $bitfields = 0;
+               $wgOut->addHtml( "<ul>" );
+
+               $where = $revObjs = array();
+               $dbr = wfGetDB( DB_SLAVE );
+               
+               $revisions = 0;
+               // Live revisions...
+               if( $this->deleteKey=='oldid' ) {
+                       // Run through and pull all our data in one query
+                       foreach( $this->revisions as $revid ) {
+                               $where[] = intval($revid);
+                       }
+                       $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( array('revision','page'), '*',
+                               array( 'rev_page' => $this->page->getArticleID(),
+                                       $whereClause, 'rev_page = page_id' ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $revObjs[$row->rev_id] = new Revision( $row );
+                       }
+                       foreach( $this->revisions as $revid ) {
+                               // Hiding top revisison is bad
+                               if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
+                                       continue;
+                               } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
+                               // If a rev is hidden from sysops
+                                       if( $action != 'submit') {
+                                               $wgOut->permissionRequired( 'suppressrevision' );
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               $revisions++;
+                               $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) );
+                               $bitfields |= $revObjs[$revid]->mDeleted;
+                       }
+               // The archives...
+               } else {
+                       // Run through and pull all our data in one query
+                       foreach( $this->archrevs as $timestamp ) {
+                               $where[] = $dbr->addQuotes( $timestamp );
+                       }
+                       $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'archive', '*',
+                               array( 'ar_namespace' => $this->page->getNamespace(),
+                                       'ar_title' => $this->page->getDBKey(),
+                                               $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $revObjs[$row->ar_timestamp] = new Revision( array(
+                               'page'       => $this->page->getArticleId(),
+                               'id'         => $row->ar_rev_id,
+                               'text'       => $row->ar_text_id,
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'text_id'    => $row->ar_text_id,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len) );
+                       }
+                       foreach( $this->archrevs as $timestamp ) {
+                               if( !isset($revObjs[$timestamp]) ) {
+                                       continue;
+                               } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
+                               // If a rev is hidden from sysops
+                                       if( $action != 'submit') {
+                                               $wgOut->permissionRequired( 'suppressrevision' );
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               $revisions++;
+                               $wgOut->addHtml( $this->historyLine( $revObjs[$timestamp] ) );
+                               $bitfields |= $revObjs[$timestamp]->mDeleted;
+                       }
+               }
+               if( !$revisions ) {
+                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                       return;
+               }
+               
+               $wgOut->addHtml( "</ul>" );
+
+               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
+
+               // Normal sysops can always see what they did, but can't always change it
+               if( !$UserAllowed ) return;
+
+               $items = array(
+                       Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+                       Xml::submitButton( wfMsg( 'revdelete-submit' ) )
+               );
+               $hidden = array(
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
+                       Xml::hidden( 'target', $this->page->getPrefixedText() ),
+                       Xml::hidden( 'type', $this->deleteKey )
+               );
+               if( $this->deleteKey=='oldid' ) {
+                       foreach( $revObjs as $rev )
+                               $hidden[] = wfHidden( 'oldid[]', $rev->getId() );
+               } else {
+                       foreach( $revObjs as $rev )
+                               $hidden[] = wfHidden( 'artimestamp[]', $rev->getTimestamp() );
+               }
+               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+               $wgOut->addHtml(
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 
+                               'id' => 'mw-revdel-form-revisions' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       xml::element( 'legend', null,  wfMsg( 'revdelete-legend' ) )
+               );
+               // FIXME: all items checked for just one rev are checked, even if not set for the others
+               foreach( $this->checks as $item ) {
+                       list( $message, $name, $field ) = $item;
+                       $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+               }
+               foreach( $items as $item ) {
+                       $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
+               }
+               foreach( $hidden as $item ) {
+                       $wgOut->addHtml( $item );
+               }
+               $wgOut->addHtml(
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+
+       }
+
+       /**
+        * This lets a user set restrictions for archived images
+        */
+       function showImages() {
+               global $wgOut, $wgUser, $action;
+
+               $UserAllowed = true;
+
+               $count = ($this->deleteKey=='oldimage') ? count($this->ofiles) : count($this->afiles);
+               $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'),
+                       $this->page->getPrefixedText(), $count ) );
+
+               $bitfields = 0;
+               $wgOut->addHtml( "<ul>" );
+
+               $where = $filesObjs = array();
+               $dbr = wfGetDB( DB_SLAVE );
+               // Live old revisions...
+               $revisions = 0;
+               if( $this->deleteKey=='oldimage' ) {
+                       // Run through and pull all our data in one query
+                       foreach( $this->ofiles as $timestamp ) {
+                               $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() );
+                       }
+                       $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'oldimage', '*',
+                               array( 'oi_name' => $this->page->getDbKey(),
+                                       $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
+                               $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
+                               $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
+                       }
+                       // Check through our images
+                       foreach( $this->ofiles as $timestamp ) {
+                               $archivename = $timestamp.'!'.$this->page->getDbKey();
+                               if( !isset($filesObjs[$archivename]) ) {
+                                       continue;
+                               } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
+                                       // If a rev is hidden from sysops
+                                       if( $action != 'submit' ) {
+                                               $wgOut->permissionRequired( 'suppressrevision' );
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               $revisions++;
+                               // Inject history info
+                               $wgOut->addHtml( $this->fileLine( $filesObjs[$archivename] ) );
+                               $bitfields |= $filesObjs[$archivename]->deleted;
+                       }
+               // Archived files...
+               } else {
+                       // Run through and pull all our data in one query
+                       foreach( $this->afiles as $id ) {
+                               $where[] = intval($id);
+                       }
+                       $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'filearchive', '*',
+                               array( 'fa_name' => $this->page->getDbKey(),
+                                       $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
+                       }
+
+                       foreach( $this->afiles as $fileid ) {
+                               if( !isset($filesObjs[$fileid]) ) {
+                                       continue;
+                               } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
+                                       // If a rev is hidden from sysops
+                                       if( $action != 'submit' ) {
+                                               $wgOut->permissionRequired( 'suppressrevision' );
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               $revisions++;
+                               // Inject history info
+                               $wgOut->addHtml( $this->archivedfileLine( $filesObjs[$fileid] ) );
+                               $bitfields |= $filesObjs[$fileid]->deleted;
+                       }
+               }
+               if( !$revisions ) {
+                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                       return;
+               }
+               
+               $wgOut->addHtml( "</ul>" );
+
+               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
+               //Normal sysops can always see what they did, but can't always change it
+               if( !$UserAllowed ) return;
+
+               $items = array(
+                       Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+                       Xml::submitButton( wfMsg( 'revdelete-submit' ) )
+               );
+               $hidden = array(
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
+                       Xml::hidden( 'target', $this->page->getPrefixedText() ),
+                       Xml::hidden( 'type', $this->deleteKey )
+               );
+               if( $this->deleteKey=='oldimage' ) {
+                       foreach( $this->ofiles as $filename )
+                               $hidden[] = wfHidden( 'oldimage[]', $filename );
+               } else {
+                       foreach( $this->afiles as $fileid )
+                               $hidden[] = wfHidden( 'fileid[]', $fileid );
+               }
+               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+               $wgOut->addHtml(
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 
+                               'id' => 'mw-revdel-form-filerevisions' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       xml::element( 'legend', null,  wfMsg( 'revdelete-legend' ) )
+               );
+               // FIXME: all items checked for just one file are checked, even if not set for the others
+               foreach( $this->checks as $item ) {
+                       list( $message, $name, $field ) = $item;
+                       $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+               }
+               foreach( $items as $item ) {
+                       $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
+               }
+               foreach( $hidden as $item ) {
+                       $wgOut->addHtml( $item );
+               }
+
+               $wgOut->addHtml(
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+       }
+
+       /**
+        * This lets a user set restrictions for log items
+        */
+       function showLogItems() {
+               global $wgOut, $wgUser, $action, $wgMessageCache;
+
+               $UserAllowed = true;
+               $wgOut->addWikiText( wfMsgExt( 'logdelete-selected', array('parsemag'), count($this->events) ) );
+
+               $bitfields = 0;
+               $wgOut->addHtml( "<ul>" );
+
+               $where = $logRows = array();
+               $dbr = wfGetDB( DB_SLAVE );
+               // Run through and pull all our data in one query
+               $logItems = 0;
+               foreach( $this->events as $logid ) {
+                       $where[] = intval($logid);
+               }
+               list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
+               $whereClause = "log_type = '$logtype' AND log_id IN(" . implode(',',$where) . ")";
+               $result = $dbr->select( 'logging', '*',
+                       array( $whereClause ),
+                       __METHOD__ );
+               while( $row = $dbr->fetchObject( $result ) ) {
+                       $logRows[$row->log_id] = $row;
+               }
+               $wgMessageCache->loadAllMessages();
+               foreach( $this->events as $logid ) {
+                       // Don't hide from oversight log!!!
+                       if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='suppress' ) {
+                               continue;
+                       } else if( !LogEventsList::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
+                       // If an event is hidden from sysops
+                               if( $action != 'submit') {
+                                       $wgOut->permissionRequired( 'suppressrevision' );
+                                       return;
+                               }
+                               $UserAllowed = false;
+                       }
+                       $logItems++;
+                       $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
+                       $bitfields |= $logRows[$logid]->log_deleted;
+               }
+               if( !$logItems ) {
+                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                       return;
+               }
+               
+               $wgOut->addHtml( "</ul>" );
+
+               $wgOut->addWikiMsg( 'revdelete-text' );
+               // Normal sysops can always see what they did, but can't always change it
+               if( !$UserAllowed ) return;
+
+               $items = array(
+                       Xml::inputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+                       Xml::submitButton( wfMsg( 'revdelete-submit' ) ) );
+               $hidden = array(
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ),
+                       Xml::hidden( 'target', $this->page->getPrefixedText() ),
+                       Xml::hidden( 'type', $this->deleteKey ) );
+               foreach( $this->events as $logid ) {
+                       $hidden[] = Xml::hidden( 'logid[]', $logid );
+               }
+
+               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+               $wgOut->addHtml(
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), 
+                               'id' => 'mw-revdel-form-logs' ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       xml::element( 'legend', null,  wfMsg( 'revdelete-legend' ) )
+               );
+               // FIXME: all items checked for just on event are checked, even if not set for the others
+               foreach( $this->checks as $item ) {
+                       list( $message, $name, $field ) = $item;
+                       $wgOut->addHtml( Xml::tags( 'div', null, Xml::checkLabel( wfMsg( $message ), $name, $name, $bitfields & $field ) ) );
+               }
+               foreach( $items as $item ) {
+                       $wgOut->addHtml( Xml::tags( 'p', null, $item ) );
+               }
+               foreach( $hidden as $item ) {
+                       $wgOut->addHtml( $item );
+               }
+
+               $wgOut->addHtml(
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+       }
+
+       /**
+        * @param Revision $rev
+        * @returns string
+        */
+       private function historyLine( $rev ) {
+               global $wgContLang;
+
+               $date = $wgContLang->timeanddate( $rev->getTimestamp() );
+               $difflink = $del = '';
+               // Live revisions
+               if( $this->deleteKey=='oldid' ) {
+                       $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() );
+                       $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
+                               'diff=' . $rev->getId() . '&oldid=prev' ) . ')';
+               // Archived revisions
+               } else {
+                       $undelete = SpecialPage::getTitleFor( 'Undelete' );
+                       $target = $this->page->getPrefixedText();
+                       $revlink = $this->skin->makeLinkObj( $undelete, $date,
+                               "target=$target&timestamp=" . $rev->getTimestamp() );
+                       $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'),
+                               "target=$target&diff=prev&timestamp=" . $rev->getTimestamp() ) . ')';
+               }
+
+               if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+                       $revlink = '<span class="history-deleted">'.$revlink.'</span>';
+                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+                       if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                               $revlink = '<span class="history-deleted">'.$date.'</span>';
+                               $difflink = '(' . wfMsgHtml('diff') . ')';
+                       }
+               }
+
+               return "<li> $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del</li>";
+       }
+
+       /**
+        * @param File $file
+        * @returns string
+        */
+       private function fileLine( $file ) {
+               global $wgContLang, $wgTitle;
+
+               $target = $this->page->getPrefixedText();
+               $date = $wgContLang->timeanddate( $file->getTimestamp(), true  );
+
+               $del = '';
+               # Hidden files...
+               if( $file->isDeleted(File::DELETED_FILE) ) {
+                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+                       if( !$file->userCan(File::DELETED_FILE) ) {
+                               $pageLink = $date;
+                       } else {
+                               $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
+                                       "target=$target&file=$file->sha1.".$file->getExtension() );
+                       }
+                       $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+               # Regular files...
+               } else {
+                       $url = $file->getUrlRel();
+                       $pageLink = "<a href=\"{$url}\">{$date}</a>";
+               }
+
+               $data = wfMsgHtml( 'widthheight',
+                                       $wgContLang->formatNum( $file->getWidth() ),
+                                       $wgContLang->formatNum( $file->getHeight() ) ) .
+                       ' (' . wfMsgHtml( 'nbytes', $wgContLang->formatNum( $file->getSize() ) ) . ')';
+
+               return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+       }
+
+       /**
+        * @param ArchivedFile $file
+        * @returns string
+        */
+       private function archivedfileLine( $file ) {
+               global $wgContLang, $wgTitle;
+
+               $target = $this->page->getPrefixedText();
+               $date = $wgContLang->timeanddate( $file->getTimestamp(), true  );
+
+               $undelete = SpecialPage::getTitleFor( 'Undelete' );
+               $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" );
+
+               $del = '';
+               if( $file->isDeleted(File::DELETED_FILE) ) {
+                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+               }
+
+               $data = wfMsgHtml( 'widthheight',
+                                       $wgContLang->formatNum( $file->getWidth() ),
+                                       $wgContLang->formatNum( $file->getHeight() ) ) .
+                       ' (' . wfMsgHtml( 'nbytes', $wgContLang->formatNum( $file->getSize() ) ) . ')';
+
+               return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+       }
+
+       /**
+        * @param Array $row row
+        * @returns string
+        */
+       private function logLine( $row ) {
+               global $wgContLang;
+
+               $date = $wgContLang->timeanddate( $row->log_timestamp );
+               $paramArray = LogPage::extractParams( $row->log_params );
+               $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+
+               $logtitle = SpecialPage::getTitleFor( 'Log' );
+               $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ),
+                       wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) );
+               // Action text
+               if( !LogEventsList::userCan($row,LogPage::DELETED_ACTION) ) {
+                       $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+               } else {
+                       $action = LogPage::actionText( $row->log_type, $row->log_action, $title,
+                               $this->skin, $paramArray, true, true );
+                       if( $row->log_deleted & LogPage::DELETED_ACTION )
+                               $action = '<span class="history-deleted">' . $action . '</span>';
+               }
+               // User links
+               $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) );
+               if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) {
+                       $userLink = '<span class="history-deleted">' . $userLink . '</span>';
+               }
+               // Comment
+               $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
+               if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) {
+                       $comment = '<span class="history-deleted">' . $comment . '</span>';
+               }
+               return "<li>($loglink) $date $userLink $action $comment</li>";
+       }
+
+       /**
+        * Generate a user tool link cluster if the current user is allowed to view it
+        * @param ArchivedFile $file
+        * @return string HTML
+        */
+       private function fileUserTools( $file ) {
+               if( $file->userCan( Revision::DELETED_USER ) ) {
+                       $link = $this->skin->userLink( $file->user, $file->user_text ) .
+                               $this->skin->userToolLinks( $file->user, $file->user_text );
+               } else {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               }
+               if( $file->isDeleted( Revision::DELETED_USER ) ) {
+                       return '<span class="history-deleted">' . $link . '</span>';
+               }
+               return $link;
+       }
+
+       /**
+        * Wrap and format the given file's comment block, if the current
+        * user is allowed to view it.
+        *
+        * @param ArchivedFile $file
+        * @return string HTML
+        */
+       private function fileComment( $file ) {
+               if( $file->userCan( File::DELETED_COMMENT ) ) {
+                       $block = $this->skin->commentBlock( $file->description );
+               } else {
+                       $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+               }
+               if( $file->isDeleted( File::DELETED_COMMENT ) ) {
+                       return "<span class=\"history-deleted\">$block</span>";
+               }
+               return $block;
+       }
+
+       /**
+        * @param WebRequest $request
+        */
+       function submit( $request ) {
+               global $wgUser, $wgOut;
+
+               $bitfield = $this->extractBitfield( $request );
+               $comment = $request->getText( 'wpReason' );
+               # Can the user set this field?
+               if( $bitfield & Revision::DELETED_RESTRICTED && !$wgUser->isAllowed('suppressrevision') ) {
+                       $wgOut->permissionRequired( 'suppressrevision' );
+                       return false;
+               }
+               # If the save went through, go to success message. Otherwise
+               # bounce back to form...
+               if( $this->save( $bitfield, $comment, $this->page ) ) {
+                       $this->success();
+               } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
+                       return $this->showRevs();
+               } else if( $request->getCheck( 'logid' ) ) {
+                       return $this->showLogs();
+               } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
+                       return $this->showImages();
+               }
+       }
+
+       private function success() {
+               global $wgOut;
+
+               $wgOut->setPagetitle( wfMsgHtml( 'actioncomplete' ) );
+
+               if( $this->deleteKey=='logid' ) {
+                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'logdelete-success' ) ), false );
+                       $this->showLogItems();
+               } else if( $this->deleteKey=='oldid' || $this->deleteKey=='artimestamp' ) {
+                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false );
+                       $this->showRevs();
+               } else if( $this->deleteKey=='fileid' ) {
+                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false );
+                       $this->showImages();
+               } else if( $this->deleteKey=='oldimage' ) {
+                       $wgOut->addWikiText( Xml::element( 'span', array( 'class' => 'success' ), wfMsg( 'revdelete-success' ) ), false );
+                       $this->showImages();
+               }
+       }
+
+       /**
+        * Put together a rev_deleted bitfield from the submitted checkboxes
+        * @param WebRequest $request
+        * @return int
+        */
+       private function extractBitfield( $request ) {
+               $bitfield = 0;
+               foreach( $this->checks as $item ) {
+                       list( /* message */ , $name, $field ) = $item;
+                       if( $request->getCheck( $name ) ) {
+                               $bitfield |= $field;
+                       }
+               }
+               return $bitfield;
+       }
+
+       private function save( $bitfield, $reason, $title ) {
+               $dbw = wfGetDB( DB_MASTER );
+               // Don't allow simply locking the interface for no reason
+               if( $bitfield == Revision::DELETED_RESTRICTED ) {
+                       $bitfield = 0;
+               }
+               $deleter = new RevisionDeleter( $dbw );
+               // By this point, only one of the below should be set
+               if( isset($this->revisions) ) {
+                       return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
+               } else if( isset($this->archrevs) ) {
+                       return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
+               } else if( isset($this->ofiles) ) {
+                       return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
+               } else if( isset($this->afiles) ) {
+                       return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
+               } else if( isset($this->events) ) {
+                       return $deleter->setEventVisibility( $title, $this->events, $bitfield, $reason );
+               }
+       }
+}
+
+/**
+ * Implements the actions for Revision Deletion.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleter {
+       function __construct( $db ) {
+               $this->dbw = $db;
+       }
+
+       /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setRevVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+
+               $userAllowedAll = $success = true;
+               $revIDs = array();
+               $revCount = 0;
+               // Run through and pull all our data in one query
+               foreach( $items as $revid ) {
+                       $where[] = intval($revid);
+               }
+               $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'revision', '*',
+                       array( 'rev_page' => $title->getArticleID(),
+                               $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $revObjs[$row->rev_id] = new Revision( $row );
+               }
+               // To work!
+               foreach( $items as $revid ) {
+                       if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
+                       }
+                       // For logging, maintain a count of revisions
+                       if( $revObjs[$revid]->mDeleted != $bitfield ) {
+                               $revCount++;
+                               $revIDs[]=$revid;
+
+                               $this->updateRevision( $revObjs[$revid], $bitfield );
+                               $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
+                       }
+               }
+               // Clear caches...
+               // Don't log or touch if nothing changed
+               if( $revCount > 0 ) {
+                       $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted,
+                               $comment, $title, 'oldid', $revIDs );
+                       $this->updatePage( $title );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       //FIXME: still might be confusing???
+                       $wgOut->permissionRequired( 'suppressrevision' );
+                       return false;
+               }
+
+               return $success;
+       }
+
+        /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+
+               $userAllowedAll = $success = true;
+               $count = 0;
+               $Id_set = array();
+               // Run through and pull all our data in one query
+               foreach( $items as $timestamp ) {
+                       $where[] = $this->dbw->addQuotes( $timestamp );
+               }
+               $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'archive', '*',
+                       array( 'ar_namespace' => $title->getNamespace(),
+                               'ar_title' => $title->getDBKey(),
+                                       $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $revObjs[$row->ar_timestamp] = new Revision( array(
+                       'page'       => $title->getArticleId(),
+                       'id'         => $row->ar_rev_id,
+                       'text'       => $row->ar_text_id,
+                       'comment'    => $row->ar_comment,
+                       'user'       => $row->ar_user,
+                       'user_text'  => $row->ar_user_text,
+                       'timestamp'  => $row->ar_timestamp,
+                       'minor_edit' => $row->ar_minor_edit,
+                       'text_id'    => $row->ar_text_id,
+                       'deleted'    => $row->ar_deleted,
+                       'len'        => $row->ar_len) );
+               }
+               // To work!
+               foreach( $items as $timestamp ) {
+                       // This will only select the first revision with this timestamp.
+                       // Since they are all selected/deleted at once, we can just check the
+                       // permissions of one. UPDATE is done via timestamp, so all revs are set.
+                       if( !is_object($revObjs[$timestamp]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
+                       }
+                       // Which revisions did we change anything about?
+                       if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
+                          $Id_set[]=$timestamp;
+                          $count++;
+
+                          $this->updateArchive( $revObjs[$timestamp], $title, $bitfield );
+                       }
+               }
+               // For logging, maintain a count of revisions
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted,
+                               $comment, $title, 'artimestamp', $Id_set );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'suppressrevision' );
+                       return false;
+               }
+
+               return $success;
+       }
+
+        /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+
+               $userAllowedAll = $success = true;
+               $count = 0;
+               $set = array();
+               // Run through and pull all our data in one query
+               foreach( $items as $timestamp ) {
+                       $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
+               }
+               $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'oldimage', '*',
+                       array( 'oi_name' => $title->getDbKey(),
+                               $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
+                       $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
+                       $filesObjs[$row->oi_archive_name]->user_text = $row->oi_user_text;
+               }
+               // To work!
+               foreach( $items as $timestamp ) {
+                       $archivename = $timestamp.'!'.$title->getDbKey();
+                       if( !isset($filesObjs[$archivename]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
+                       }
+
+                       $transaction = true;
+                       // Which revisions did we change anything about?
+                       if( $filesObjs[$archivename]->deleted != $bitfield ) {
+                               $count++;
+
+                               $this->dbw->begin();
+                               $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
+                               // If this image is currently hidden...
+                               if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
+                                       if( $bitfield & File::DELETED_FILE ) {
+                                               # Leave it alone if we are not changing this...
+                                               $set[]=$archivename;
+                                               $transaction = true;
+                                       } else {
+                                               # We are moving this out
+                                               $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
+                                               $set[]=$transaction;
+                                       }
+                               // Is it just now becoming hidden?
+                               } else if( $bitfield & File::DELETED_FILE ) {
+                                       $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
+                                       $set[]=$transaction;
+                               } else {
+                                       $set[]=$timestamp;
+                               }
+                               // If our file operations fail, then revert back the db
+                               if( $transaction==false ) {
+                                       $this->dbw->rollback();
+                                       return false;
+                               }
+                               $this->dbw->commit();
+                       }
+               }
+
+               // Log if something was changed
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted,
+                               $comment, $title, 'oldimage', $set );
+                       # Purge page/history
+                       $file = wfLocalFile( $title );
+                       $file->purgeCache();
+                       $file->purgeHistory();
+                       # Invalidate cache for all pages using this file
+                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
+                       $update->doUpdate();
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'suppressrevision' );
+                       return false;
+               }
+
+               return $success;
+       }
+
+        /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+
+               $userAllowedAll = $success = true;
+               $count = 0;
+               $Id_set = array();
+
+               // Run through and pull all our data in one query
+               foreach( $items as $id ) {
+                       $where[] = intval($id);
+               }
+               $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'filearchive', '*',
+                       array( 'fa_name' => $title->getDbKey(),
+                               $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
+               }
+               // To work!
+               foreach( $items as $fileid ) {
+                       if( !isset($filesObjs[$fileid]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
+                       }
+                       // Which revisions did we change anything about?
+                       if( $filesObjs[$fileid]->deleted != $bitfield ) {
+                          $Id_set[]=$fileid;
+                          $count++;
+
+                          $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
+                       }
+               }
+               // Log if something was changed
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $comment,
+                               $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'suppressrevision' );
+                       return false;
+               }
+
+               return $success;
+       }
+
+       /**
+        * @param $title, the log page these events apply to
+        * @param array $items list of log ID numbers
+        * @param int $bitfield new log_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setEventVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+
+               $userAllowedAll = $success = true;
+               $count = 0;
+               $log_Ids = array();
+
+               // Run through and pull all our data in one query
+               foreach( $items as $logid ) {
+                       $where[] = intval($logid);
+               }
+               list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
+               $whereClause = "log_type ='$logtype' AND log_id IN(" . implode(',',$where) . ")";
+               $result = $this->dbw->select( 'logging', '*',
+                       array( $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $logRows[$row->log_id] = $row;
+               }
+               // To work!
+               foreach( $items as $logid ) {
+                       if( !isset($logRows[$logid]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !LogEventsList::userCan($logRows[$logid], LogPage::DELETED_RESTRICTED)
+                                || $logRows[$logid]->log_type == 'suppress' ) {
+                       // Don't hide from oversight log!!!
+                       $userAllowedAll=false;
+                       continue;
+                       }
+                       // Which logs did we change anything about?
+                       if( $logRows[$logid]->log_deleted != $bitfield ) {
+                               $log_Ids[]=$logid;
+                               $count++;
+
+                               $this->updateLogs( $logRows[$logid], $bitfield );
+                               $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
+                       }
+               }
+               // Don't log or touch if nothing changed
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $logRows[$logid]->log_deleted,
+                               $comment, $title, 'logid', $log_Ids );
+               }
+               // Were all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'suppressrevision' );
+                       return false;
+               }
+
+               return $success;
+       }
+
+       /**
+        * Moves an image to a safe private location
+        * Caller is responsible for clearing caches
+        * @param File $oimage
+        * @returns mixed, timestamp string on success, false on failure
+        */
+       function makeOldImagePrivate( $oimage ) {
+               $transaction = new FSTransaction();
+               if( !FileStore::lock() ) {
+                       wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
+                       return false;
+               }
+               $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
+               // Dupe the file into the file store
+               if( file_exists( $oldpath ) ) {
+                       // Is our directory configured?
+                       if( $store = FileStore::get( 'deleted' ) ) {
+                               if( !$oimage->sha1 ) {
+                                       $oimage->upgradeRow(); // sha1 may be missing
+                               }
+                               $key = $oimage->sha1 . '.' . $oimage->getExtension();
+                               $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
+                       } else {
+                               $group = null;
+                               $key = null;
+                               $transaction = false; // Return an error and do nothing
+                       }
+               } else {
+                       wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
+                       $group = null;
+                       $key = '';
+                       $transaction = new FSTransaction(); // empty
+               }
+
+               if( $transaction === false ) {
+                       // Fail to restore?
+                       wfDebug( __METHOD__.": import to file store failed, aborting\n" );
+                       throw new MWException( "Could not archive and delete file $oldpath" );
+                       return false;
+               }
+
+               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+               $transaction->commit();
+               FileStore::unlock();
+
+               $m = explode('!',$oimage->archive_name,2);
+               $timestamp = $m[0];
+
+               return $timestamp;
+       }
+
+       /**
+        * Moves an image from a safe private location
+        * Caller is responsible for clearing caches
+        * @param File $oimage
+        * @returns mixed, string timestamp on success, false on failure
+        */
+       function makeOldImagePublic( $oimage ) {
+               $transaction = new FSTransaction();
+               if( !FileStore::lock() ) {
+                       wfDebug( __METHOD__." could not acquire filestore lock\n" );
+                       return false;
+               }
+
+               $store = FileStore::get( 'deleted' );
+               if( !$store ) {
+                       wfDebug( __METHOD__.": skipping row with no file.\n" );
+                       return false;
+               }
+
+               $key = $oimage->sha1.'.'.$oimage->getExtension();
+               $destDir = $oimage->getArchivePath();
+               if( !is_dir( $destDir ) ) {
+                       wfMkdirParents( $destDir );
+               }
+               $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
+               // Check if any other stored revisions use this file;
+               // if so, we shouldn't remove the file from the hidden
+               // archives so they will still work. Check hidden files first.
+               $useCount = $this->dbw->selectField( 'oldimage', '1',
+                       array( 'oi_sha1' => $oimage->sha1,
+                               'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
+                       __METHOD__, array( 'FOR UPDATE' ) );
+               // Check the rest of the deleted archives too.
+               // (these are the ones that don't show in the image history)
+               if( !$useCount ) {
+                       $useCount = $this->dbw->selectField( 'filearchive', '1',
+                               array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
+                               __METHOD__, array( 'FOR UPDATE' ) );
+               }
+
+               if( $useCount == 0 ) {
+                       wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
+                       $flags = FileStore::DELETE_ORIGINAL;
+               } else {
+                       $flags = 0;
+               }
+               $transaction->add( $store->export( $key, $destPath, $flags ) );
+
+               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+               $transaction->commit();
+               FileStore::unlock();
+
+               $m = explode('!',$oimage->archive_name,2);
+               $timestamp = $m[0];
+
+               return $timestamp;
+       }
+
+       /**
+        * Update the revision's rev_deleted field
+        * @param Revision $rev
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateRevision( $rev, $bitfield ) {
+               $this->dbw->update( 'revision',
+                       array( 'rev_deleted' => $bitfield ),
+                       array( 'rev_id' => $rev->getId(),
+                               'rev_page' => $rev->getPage() ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Update the revision's rev_deleted field
+        * @param Revision $rev
+        * @param Title $title
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateArchive( $rev, $title, $bitfield ) {
+               $this->dbw->update( 'archive',
+                       array( 'ar_deleted' => $bitfield ),
+                       array( 'ar_namespace' => $title->getNamespace(),
+                               'ar_title'     => $title->getDBKey(),
+                               'ar_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ),
+                               'ar_rev_id' => $rev->getId() ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Update the images's oi_deleted field
+        * @param File $file
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateOldFiles( $file, $bitfield ) {
+               $this->dbw->update( 'oldimage',
+                       array( 'oi_deleted' => $bitfield ),
+                       array( 'oi_name' => $file->getName(),
+                               'oi_timestamp' => $this->dbw->timestamp( $file->getTimestamp() ) ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Update the images's fa_deleted field
+        * @param ArchivedFile $file
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateArchFiles( $file, $bitfield ) {
+               $this->dbw->update( 'filearchive',
+                       array( 'fa_deleted' => $bitfield ),
+                       array( 'fa_id' => $file->getId() ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Update the logging log_deleted field
+        * @param Row $row
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateLogs( $row, $bitfield ) {
+               $this->dbw->update( 'logging',
+                       array( 'log_deleted' => $bitfield ),
+                       array( 'log_id' => $row->log_id ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Update the revision's recentchanges record if fields have been hidden
+        * @param Revision $rev
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateRecentChangesEdits( $rev, $bitfield ) {
+               $this->dbw->update( 'recentchanges',
+                       array( 'rc_deleted' => $bitfield,
+                                  'rc_patrolled' => 1 ),
+                       array( 'rc_this_oldid' => $rev->getId(),
+                               'rc_timestamp' => $this->dbw->timestamp( $rev->getTimestamp() ) ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Update the revision's recentchanges record if fields have been hidden
+        * @param Row $row
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateRecentChangesLog( $row, $bitfield ) {
+               $this->dbw->update( 'recentchanges',
+                       array( 'rc_deleted' => $bitfield,
+                                  'rc_patrolled' => 1 ),
+                       array( 'rc_logid' => $row->log_id,
+                               'rc_timestamp' => $row->log_timestamp ),
+                       __METHOD__ );
+       }
+
+       /**
+        * Touch the page's cache invalidation timestamp; this forces cached
+        * history views to refresh, so any newly hidden or shown fields will
+        * update properly.
+        * @param Title $title
+        */
+       function updatePage( $title ) {
+               $title->invalidateCache();
+               $title->purgeSquid();
+
+               // Extensions that require referencing previous revisions may need this
+               wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
+       }
+
+       /**
+        * Checks for a change in the bitfield for a certain option and updates the
+        * provided array accordingly.
+        *
+        * @param String $desc Description to add to the array if the option was
+        * enabled / disabled.
+        * @param int $field The bitmask describing the single option.
+        * @param int $diff The xor of the old and new bitfields.
+        * @param array $arr The array to update.
+        */
+       function checkItem ( $desc, $field, $diff, $new, &$arr ) {
+               if ( $diff & $field ) {
+                       $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc;
+               }
+       }
+
+       /**
+        * Gets an array describing the changes made to the visibilit of the revision.
+        * If the resulting array is $arr, then $arr[0] will contain an array of strings
+        * describing the items that were hidden, $arr[2] will contain an array of strings
+        * describing the items that were unhidden, and $arr[3] will contain an array with
+        * a single string, which can be one of "applied restrictions to sysops",
+        * "removed restrictions from sysops", or null.
+        *
+        * @param int $n The new bitfield.
+        * @param int $o The old bitfield.
+        * @return An array as described above.
+        */
+       function getChanges ( $n, $o ) {
+               $diff = $n ^ $o;
+               $ret = array ( 0 => array(), 1 => array(), 2 => array() );
+
+               $this->checkItem ( wfMsgForContent ( 'revdelete-content' ),
+                               Revision::DELETED_TEXT, $diff, $n, $ret );
+               $this->checkItem ( wfMsgForContent ( 'revdelete-summary' ),
+                               Revision::DELETED_COMMENT, $diff, $n, $ret );
+               $this->checkItem ( wfMsgForContent ( 'revdelete-uname' ),
+                               Revision::DELETED_USER, $diff, $n, $ret );
+
+               // Restriction application to sysops
+               if ( $diff & Revision::DELETED_RESTRICTED ) {
+                       if ( $n & Revision::DELETED_RESTRICTED )
+                               $ret[2][] = wfMsgForContent ( 'revdelete-restricted' );
+                       else
+                               $ret[2][] = wfMsgForContent ( 'revdelete-unrestricted' );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Gets a log message to describe the given revision visibility change. This
+        * message will be of the form "[hid {content, edit summary, username}];
+        * [unhid {...}][applied restrictions to sysops] for $count revisions: $comment".
+        *
+        * @param int $count The number of effected revisions.
+        * @param int $nbitfield The new bitfield for the revision.
+        * @param int $obitfield The old bitfield for the revision.
+        * @param string $comment The comment associated with the change.
+        * @param bool $isForLog
+        */
+       function getLogMessage ( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) {
+               global $wgContLang;
+
+               $s = '';
+               $changes = $this->getChanges( $nbitfield, $obitfield );
+
+               if ( count ( $changes[0] ) ) {
+                       $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) );
+               }
+
+               if ( count ( $changes[1] ) ) {
+                       if ($s) $s .= '; ';
+
+                       $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) );
+               }
+
+               if ( count ( $changes[2] )) {
+                       if ($s)
+                               $s .= ' (' . $changes[2][0] . ')';
+                       else
+                               $s = $changes[2][0];
+               }
+
+               $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
+               $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
+                       $s, $wgContLang->formatNum( $count ) );
+
+               if ( $comment )
+                       $ret .= ": $comment";
+
+               return $ret;
+
+       }
+
+       /**
+        * Record a log entry on the action
+        * @param Title $title, page where item was removed from
+        * @param int $count the number of revisions altered for this page
+        * @param int $nbitfield the new _deleted value
+        * @param int $obitfield the old _deleted value
+        * @param string $comment
+        * @param Title $target, the relevant page
+        * @param string $param, URL param
+        * @param Array $items
+        */
+       function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) {
+               // Put things hidden from sysops in the oversight log
+               $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'suppress' : 'delete';
+               $log = new LogPage( $logtype );
+
+               $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
+
+               if( $param == 'logid' ) {
+                       $params = array( implode( ',', $items) );
+                       $log->addEntry( 'event', $title, $reason, $params );
+               } else {
+                       // Add params for effected page and ids
+                       $params = array( $param, implode( ',', $items) );
+                       $log->addEntry( 'revision', $title, $reason, $params );
+               }
+       }
+}
diff --git a/includes/specials/Search.php b/includes/specials/Search.php
new file mode 100644 (file)
index 0000000..0a483af
--- /dev/null
@@ -0,0 +1,651 @@
+<?php
+# Copyright (C) 2004 Brion Vibber <brion@pobox.com>
+# http://www.mediawiki.org/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# http://www.gnu.org/copyleft/gpl.html
+
+/**
+ * Run text & title search and display the output
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point
+ *
+ * @param $par String: (default '')
+ */
+function wfSpecialSearch( $par = '' ) {
+       global $wgRequest, $wgUser;
+
+       $search = str_replace( "\n", " ", $wgRequest->getText( 'search', $par ) );
+       $searchPage = new SpecialSearch( $wgRequest, $wgUser );
+       if( $wgRequest->getVal( 'fulltext' ) 
+               || !is_null( $wgRequest->getVal( 'offset' )) 
+               || !is_null( $wgRequest->getVal( 'searchx' ))) {
+               $searchPage->showResults( $search, 'search' );
+       } else {
+               $searchPage->goResult( $search );
+       }
+}
+
+/**
+ * implements Special:Search - Run text & title search and display the output
+ * @ingroup SpecialPage
+ */
+class SpecialSearch {
+
+       /**
+        * Set up basic search parameters from the request and user settings.
+        * Typically you'll pass $wgRequest and $wgUser.
+        *
+        * @param WebRequest $request
+        * @param User $user
+        * @public
+        */
+       function SpecialSearch( &$request, &$user ) {
+               list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
+
+               $this->namespaces = $this->powerSearch( $request );
+               if( empty( $this->namespaces ) ) {
+                       $this->namespaces = SearchEngine::userNamespaces( $user );
+               }
+
+               $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
+       }
+
+       /**
+        * If an exact title match can be found, jump straight ahead to it.
+        * @param string $term
+        * @public
+        */
+       function goResult( $term ) {
+               global $wgOut;
+               global $wgGoToEdit;
+
+               $this->setupPage( $term );
+
+               # Try to go to page as entered.
+               $t = Title::newFromText( $term );
+
+               # If the string cannot be used to create a title
+               if( is_null( $t ) ){
+                       return $this->showResults( $term );
+               }
+
+               # If there's an exact or very near match, jump right there.
+               $t = SearchEngine::getNearMatch( $term );
+               if( !is_null( $t ) ) {
+                       $wgOut->redirect( $t->getFullURL() );
+                       return;
+               }
+
+               # No match, generate an edit URL
+               $t = Title::newFromText( $term );
+               if( ! is_null( $t ) ) {
+                       wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
+                       # If the feature is enabled, go straight to the edit page
+                       if ( $wgGoToEdit ) {
+                               $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
+                               return;
+                       }
+               }
+
+               $wgOut->wrapWikiMsg( "==$1==\n", 'notitlematches' );
+               if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
+                       $wgOut->addWikiMsg( 'noexactmatch', wfEscapeWikiText( $term ) );
+               } else {
+                       $wgOut->addWikiMsg( 'noexactmatch-nocreate', wfEscapeWikiText( $term ) );
+               }
+
+               return $this->showResults( $term );
+       }
+
+       /**
+        * @param string $term
+        * @public
+        */
+       function showResults( $term ) {
+               $fname = 'SpecialSearch::showResults';
+               wfProfileIn( $fname );
+               global $wgOut, $wgUser;
+               $sk = $wgUser->getSkin();
+
+               $this->setupPage( $term );
+
+               $wgOut->addWikiMsg( 'searchresulttext' );
+
+               if( '' === trim( $term ) ) {
+                       // Empty query -- straight view of search form
+                       $wgOut->setSubtitle( '' );
+                       $wgOut->addHTML( $this->powerSearchBox( $term ) );
+                       $wgOut->addHTML( $this->powerSearchFocus() );
+                       wfProfileOut( $fname );
+                       return;
+               }
+
+               global $wgDisableTextSearch;
+               if ( $wgDisableTextSearch ) {
+                       global $wgForwardSearchUrl;
+                       if( $wgForwardSearchUrl ) {
+                               $url = str_replace( '$1', urlencode( $term ), $wgForwardSearchUrl );
+                               $wgOut->redirect( $url );
+                               return;
+                       }
+                       global $wgInputEncoding;
+                       $wgOut->addHTML(
+                               Xml::openElement( 'fieldset' ) .
+                               Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
+                               Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
+                               wfMsg( 'googlesearch',
+                                       htmlspecialchars( $term ),
+                                       htmlspecialchars( $wgInputEncoding ),
+                                       htmlspecialchars( wfMsg( 'searchbutton' ) )
+                               ) .
+                               Xml::closeElement( 'fieldset' )
+                       );
+                       wfProfileOut( $fname );
+                       return;
+               }
+
+               $wgOut->addHTML( $this->shortDialog( $term ) );
+
+               $search = SearchEngine::create();
+               $search->setLimitOffset( $this->limit, $this->offset );
+               $search->setNamespaces( $this->namespaces );
+               $search->showRedirects = $this->searchRedirects;
+               $rewritten = $search->replacePrefixes($term);
+
+               $titleMatches = $search->searchTitle( $rewritten );
+
+               // Sometimes the search engine knows there are too many hits
+               if ($titleMatches instanceof SearchResultTooMany) {
+                       $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
+                       $wgOut->addHTML( $this->powerSearchBox( $term ) );
+                       $wgOut->addHTML( $this->powerSearchFocus() );
+                       wfProfileOut( $fname );
+                       return;
+               }
+               
+               $textMatches = $search->searchText( $rewritten );
+
+               // did you mean... suggestions
+               if($textMatches && $textMatches->hasSuggestion()){
+                       $st = SpecialPage::getTitleFor( 'Search' );                     
+                       $stParams = wfArrayToCGI( array( 
+                                       'search'        => $textMatches->getSuggestionQuery(), 
+                                       'fulltext'      => wfMsg('search')),
+                                       $this->powerSearchOptions());
+                                       
+                       $suggestLink = '<a href="'.$st->escapeLocalURL($stParams).'">'.
+                                       $textMatches->getSuggestionSnippet().'</a>';
+                                       
+                       $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
+               }
+
+               // show number of results
+               $num = ( $titleMatches ? $titleMatches->numRows() : 0 )
+                       + ( $textMatches ? $textMatches->numRows() : 0);
+               $totalNum = 0;
+               if($titleMatches && !is_null($titleMatches->getTotalHits()))
+                       $totalNum += $titleMatches->getTotalHits();
+               if($textMatches && !is_null($textMatches->getTotalHits()))
+                       $totalNum += $textMatches->getTotalHits();
+               if ( $num > 0 ) {
+                       if ( $totalNum > 0 ){
+                               $top = wfMsgExt('showingresultstotal', array( 'parseinline' ), 
+                                       $this->offset+1, $this->offset+$num, $totalNum );
+                       } elseif ( $num >= $this->limit ) {
+                               $top = wfShowingResults( $this->offset, $this->limit );
+                       } else {
+                               $top = wfShowingResultsNum( $this->offset, $this->limit, $num );
+                       }
+                       $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
+               }
+
+               // prev/next links
+               if( $num || $this->offset ) {
+                       $prevnext = wfViewPrevNext( $this->offset, $this->limit,
+                               SpecialPage::getTitleFor( 'Search' ),
+                               wfArrayToCGI(
+                                       $this->powerSearchOptions(),
+                                       array( 'search' => $term ) ),
+                                       ($num < $this->limit) );
+                       $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
+                       wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
+               } else {
+                       wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
+               }
+
+               if( $titleMatches ) {
+                       if( $titleMatches->numRows() ) {
+                               $wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
+                               $wgOut->addHTML( $this->showMatches( $titleMatches ) );
+                       }
+                       $titleMatches->free();
+               }
+
+               if( $textMatches ) {
+                       // output appropriate heading
+                       if( $textMatches->numRows() ) {
+                               if($titleMatches)
+                                       $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
+                               else // if no title matches the heading is redundant
+                                       $wgOut->addHTML("<hr/>");                                                               
+                       } elseif( $num == 0 ) {
+                               # Don't show the 'no text matches' if we received title matches
+                               $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
+                       }
+                       // show interwiki results if any
+                       if( $textMatches->hasInterwikiResults() )
+                               $wgOut->addHtml( $this->showInterwiki( $textMatches->getInterwikiResults(), $term ));
+                       // show results
+                       if( $textMatches->numRows() )
+                               $wgOut->addHTML( $this->showMatches( $textMatches ) );
+
+                       $textMatches->free();
+               }
+
+               if ( $num == 0 ) {
+                       $wgOut->addWikiMsg( 'nonefound' );
+               }
+               if( $num || $this->offset ) {
+                       $wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
+               }
+               $wgOut->addHTML( $this->powerSearchBox( $term ) );
+               wfProfileOut( $fname );
+       }
+
+       #------------------------------------------------------------------
+       # Private methods below this line
+       
+       /**
+        *
+        */
+       function setupPage( $term ) {
+               global $wgOut;
+               if( !empty( $term ) )
+                       $wgOut->setPageTitle( wfMsg( 'searchresults' ) );                       
+               $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
+               $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
+               $wgOut->setArticleRelated( false );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+       }
+
+       /**
+        * Extract "power search" namespace settings from the request object,
+        * returning a list of index numbers to search.
+        *
+        * @param WebRequest $request
+        * @return array
+        * @private
+        */
+       function powerSearch( &$request ) {
+               $arr = array();
+               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+                       if( $request->getCheck( 'ns' . $ns ) ) {
+                               $arr[] = $ns;
+                       }
+               }
+               return $arr;
+       }
+
+       /**
+        * Reconstruct the 'power search' options for links
+        * @return array
+        * @private
+        */
+       function powerSearchOptions() {
+               $opt = array();
+               foreach( $this->namespaces as $n ) {
+                       $opt['ns' . $n] = 1;
+               }
+               $opt['redirs'] = $this->searchRedirects ? 1 : 0;
+               return $opt;
+       }
+
+       /**
+        * Show whole set of results 
+        * 
+        * @param SearchResultSet $matches
+        */
+       function showMatches( &$matches ) {
+               $fname = 'SpecialSearch::showMatches';
+               wfProfileIn( $fname );
+
+               global $wgContLang;
+               $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
+
+               $out = "";
+               
+               $infoLine = $matches->getInfo();
+               if( !is_null($infoLine) )
+                       $out .= "\n<!-- {$infoLine} -->\n";
+                       
+               
+               $off = $this->offset + 1;
+               $out .= "<ul class='mw-search-results'>\n";
+
+               while( $result = $matches->next() ) {
+                       $out .= $this->showHit( $result, $terms );
+               }
+               $out .= "</ul>\n";
+
+               // convert the whole thing to desired language variant
+               global $wgContLang;
+               $out = $wgContLang->convert( $out );
+               wfProfileOut( $fname );
+               return $out;
+       }
+
+       /**
+        * Format a single hit result
+        * @param SearchResult $result
+        * @param array $terms terms to highlight
+        */
+       function showHit( $result, $terms ) {
+               $fname = 'SpecialSearch::showHit';
+               wfProfileIn( $fname );
+               global $wgUser, $wgContLang, $wgLang;
+               
+               if( $result->isBrokenTitle() ) {
+                       wfProfileOut( $fname );
+                       return "<!-- Broken link in search result -->\n";
+               }
+               
+               $t = $result->getTitle();
+               $sk = $wgUser->getSkin();
+
+               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+
+               //If page content is not readable, just return the title.
+               //This is not quite safe, but better than showing excerpts from non-readable pages
+               //Note that hiding the entry entirely would screw up paging.
+               if (!$t->userCanRead()) {
+                       wfProfileOut( $fname );
+                       return "<li>{$link}</li>\n";
+               }
+
+               // If the page doesn't *exist*... our search index is out of date.
+               // The least confusing at this point is to drop the result.
+               // You may get less results, but... oh well. :P
+               if( $result->isMissingRevision() ) {
+                       wfProfileOut( $fname );
+                       return "<!-- missing page " .
+                               htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
+               }
+
+               // format redirects / relevant sections
+               $redirectTitle = $result->getRedirectTitle();
+               $redirectText = $result->getRedirectSnippet($terms);
+               $sectionTitle = $result->getSectionTitle();
+               $sectionText = $result->getSectionSnippet($terms);
+               $redirect = '';
+               if( !is_null($redirectTitle) )
+                       $redirect = "<span class='searchalttitle'>"
+                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+                               ."</span>";
+               $section = '';
+               if( !is_null($sectionTitle) )
+                       $section = "<span class='searchalttitle'>" 
+                               .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText))
+                               ."</span>";
+
+               // format text extract
+               $extract = "<div class='searchresult'>".$result->getTextSnippet($terms)."</div>";
+               
+               // format score
+               if( is_null( $result->getScore() ) ) {
+                       // Search engine doesn't report scoring info
+                       $score = '';
+               } else {
+                       $percent = sprintf( '%2.1f', $result->getScore() * 100 );
+                       $score = wfMsg( 'search-result-score', $wgLang->formatNum( $percent ) )
+                               . ' - ';
+               }
+
+               // format description
+               $byteSize = $result->getByteSize();
+               $wordCount = $result->getWordCount();
+               $timestamp = $result->getTimestamp();
+               $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
+                       $sk->formatSize( $byteSize ),
+                       $wordCount );
+               $date = $wgLang->timeanddate( $timestamp );
+
+               // link to related articles if supported
+               $related = '';
+               if( $result->hasRelated() ){
+                       $st = SpecialPage::getTitleFor( 'Search' );
+                       $stParams = wfArrayToCGI( $this->powerSearchOptions(),
+                               array('search'    => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
+                                     'fulltext'  => wfMsg('search') ));
+                       
+                       $related = ' -- <a href="'.$st->escapeLocalURL($stParams).'">'. 
+                               wfMsg('search-relatedarticle').'</a>';
+               }
+                               
+               // Include a thumbnail for media files...
+               if( $t->getNamespace() == NS_IMAGE ) {
+                       $img = wfFindFile( $t );
+                       if( $img ) {
+                               $thumb = $img->getThumbnail( 120, 120 );
+                               if( $thumb ) {
+                                       $desc = $img->getShortDesc();
+                                       wfProfileOut( $fname );
+                                       // Ugly table. :D
+                                       // Float doesn't seem to interact well with the bullets.
+                                       // Table messes up vertical alignment of the bullet, but I'm
+                                       // not sure what more I can do about that. :(
+                                       return "<li>" .
+                                               '<table class="searchResultImage">' .
+                                               '<tr>' .
+                                               '<td width="120" align="center">' .
+                                               $thumb->toHtml( array( 'desc-link' => true ) ) .
+                                               '</td>' .
+                                               '<td valign="top">' .
+                                               $link .
+                                               $extract .
+                                               "<div class='mw-search-result-data'>{$score}{$desc} - {$date}{$related}</div>" .
+                                               '</td>' .
+                                               '</tr>' .
+                                               '</table>' .
+                                               "</li>\n";
+                               }
+                       }
+               }
+
+               wfProfileOut( $fname );
+               return "<li>{$link} {$redirect} {$section} {$extract}\n" .
+                       "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
+                       "</li>\n";
+
+       }
+
+       /**
+        * Show results from other wikis
+        * 
+        * @param SearchResultSet $matches
+        */
+       function showInterwiki( &$matches, $query ) {
+               $fname = 'SpecialSearch::showInterwiki';
+               wfProfileIn( $fname );
+
+               global $wgContLang;
+               $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
+
+               $out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".wfMsg('search-interwiki-caption')."</div>\n";             
+               $off = $this->offset + 1;
+               $out .= "<ul start='{$off}' class='mw-search-iwresults'>\n";
+
+               // work out custom project captions
+               $customCaptions = array();
+               $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line <iwprefix>:<caption>
+               foreach($customLines as $line){
+                       $parts = explode(":",$line,2);
+                       if(count($parts) == 2) // validate line
+                               $customCaptions[$parts[0]] = $parts[1]; 
+               }
+               
+               
+               $prev = null;
+               while( $result = $matches->next() ) {
+                       $out .= $this->showInterwikiHit( $result, $prev, $terms, $query, $customCaptions );
+                       $prev = $result->getInterwikiPrefix();
+               }
+               // FIXME: should support paging in a non-confusing way (not sure how though, maybe via ajax)..
+               $out .= "</ul></div>\n";
+
+               // convert the whole thing to desired language variant
+               global $wgContLang;
+               $out = $wgContLang->convert( $out );
+               wfProfileOut( $fname );
+               return $out;
+       }
+       
+       /**
+        * Show single interwiki link
+        *
+        * @param SearchResult $result
+        * @param string $lastInterwiki
+        * @param array $terms
+        * @param string $query 
+        * @param array $customCaptions iw prefix -> caption
+        */
+       function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions){
+               $fname = 'SpecialSearch::showInterwikiHit';
+               wfProfileIn( $fname );
+               global $wgUser, $wgContLang, $wgLang;
+               
+               if( $result->isBrokenTitle() ) {
+                       wfProfileOut( $fname );
+                       return "<!-- Broken link in search result -->\n";
+               }
+               
+               $t = $result->getTitle();
+               $sk = $wgUser->getSkin();
+               
+               $link = $sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+                               
+               // format redirect if any
+               $redirectTitle = $result->getRedirectTitle();
+               $redirectText = $result->getRedirectSnippet($terms);
+               $redirect = '';
+               if( !is_null($redirectTitle) )
+                       $redirect = "<span class='searchalttitle'>"
+                               .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText))
+                               ."</span>";
+
+               $out = "";
+               // display project name 
+               if(is_null($lastInterwiki) || $lastInterwiki != $t->getInterwiki()){
+                       if( key_exists($t->getInterwiki(),$customCaptions) )
+                               // captions from 'search-interwiki-custom'
+                               $caption = $customCaptions[$t->getInterwiki()];
+                       else{
+                               // default is to show the hostname of the other wiki which might suck 
+                               // if there are many wikis on one hostname
+                               $parsed = parse_url($t->getFullURL());
+                               $caption = wfMsg('search-interwiki-default', $parsed['host']); 
+                       }               
+                       // "more results" link (special page stuff could be localized, but we might not know target lang)
+                       $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");                        
+                       $searchLink = $sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
+                               wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search'))); 
+                       $out .= "</ul><div class='mw-search-interwiki-project'><span class='mw-search-interwiki-more'>{$searchLink}</span>{$caption}</div>\n<ul>";
+               }
+
+               $out .= "<li>{$link} {$redirect}</li>\n"; 
+               wfProfileOut( $fname );
+               return $out;
+       }
+       
+
+       /**
+        * Generates the power search box at bottom of [[Special:Search]]
+        * @param $term string: search term
+        * @return $out string: HTML form
+        */
+       function powerSearchBox( $term ) {
+               global $wgScript;
+
+               $namespaces = '';
+               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+                       $name = str_replace( '_', ' ', $name );
+                       if( '' == $name ) {
+                               $name = wfMsg( 'blanknamespace' );
+                       }
+                       $namespaces .= Xml::openElement( 'span', array( 'style' => 'white-space: nowrap' ) ) .
+                                       Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
+                                       Xml::closeElement( 'span' ) . "\n";
+               }
+
+               $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
+               $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
+               $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
+               $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n";
+
+               $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
+                       Xml::fieldset( wfMsg( 'powersearch-legend' ),
+                               Xml::hidden( 'title', 'Special:Search' ) .
+                               "<p>" .
+                               wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
+                               "<br />" .
+                               $namespaces .
+                               "</p>" .
+                               "<p>" .
+                               $redirect . " " . $redirectLabel .
+                               "</p>" .
+                               wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
+                               "&nbsp;" .
+                               $searchField .
+                               "&nbsp;" .
+                               $searchButton ) .
+                       "</form>";
+
+               return $out;
+       }
+
+       function powerSearchFocus() {
+               global $wgJsMimeType;
+               return "<script type=\"$wgJsMimeType\">" .
+                       "hookEvent(\"load\", function(){" .
+                               "document.getElementById('powerSearchText').focus();" .
+                       "});" .
+                       "</script>";
+       }
+
+       function shortDialog($term) {
+               global $wgScript;
+
+               $out  = Xml::openElement( 'form', array(
+                       'id' => 'search',
+                       'method' => 'get',
+                       'action' => $wgScript
+               ));
+               $out .= Xml::hidden( 'title', 'Special:Search' );
+               $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . ' ';
+               foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
+                       if( in_array( $ns, $this->namespaces ) ) {
+                               $out .= Xml::hidden( "ns{$ns}", '1' );
+                       }
+               }
+               $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
+               $out .= Xml::closeElement( 'form' );
+
+               return $out;
+       }
+}
diff --git a/includes/specials/Shortpages.php b/includes/specials/Shortpages.php
new file mode 100644 (file)
index 0000000..2e7d24a
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * SpecialShortpages extends QueryPage. It is used to return the shortest
+ * pages in the database.
+ * @ingroup SpecialPage
+ */
+class ShortPagesPage extends QueryPage {
+
+       function getName() {
+               return 'Shortpages';
+       }
+
+       /**
+        * This query is indexed as of 1.5
+        */
+       function isExpensive() {
+               return true;
+       }
+
+       function isSyndicated() {
+               return false;
+       }
+
+       function getSQL() {
+               global $wgContentNamespaces;
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $page = $dbr->tableName( 'page' );
+               $name = $dbr->addQuotes( $this->getName() );
+
+               $forceindex = $dbr->useIndexClause("page_len");
+
+               if ($wgContentNamespaces)
+                       $nsclause = "page_namespace IN (" . $dbr->makeList($wgContentNamespaces) . ")";
+               else
+                       $nsclause = "page_namespace = " . NS_MAIN;
+
+               return
+                       "SELECT $name as type,
+                               page_namespace as namespace,
+                               page_title as title,
+                               page_len AS value
+                       FROM $page $forceindex
+                       WHERE $nsclause AND page_is_redirect=0";
+       }
+
+       function preprocessResults( $db, $res ) {
+               # There's no point doing a batch check if we aren't caching results;
+               # the page must exist for it to have been pulled out of the table
+               if( $this->isCached() ) {
+                       $batch = new LinkBatch();
+                       while( $row = $db->fetchObject( $res ) )
+                               $batch->add( $row->namespace, $row->title );
+                       $batch->execute();
+                       if( $db->numRows( $res ) > 0 )
+                               $db->dataSeek( $res, 0 );
+               }
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+               $dm = $wgContLang->getDirMark();
+
+               $title = Title::makeTitleSafe( $result->namespace, $result->title );
+               if ( !$title ) {
+                       return '<!-- Invalid title ' .  htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->';
+               }
+               $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+               $plink = $this->isCached()
+                                       ? $skin->makeLinkObj( $title )
+                                       : $skin->makeKnownLinkObj( $title );
+               $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) );
+
+               return $title->exists()
+                               ? "({$hlink}) {$dm}{$plink} {$dm}[{$size}]"
+                               : "<s>({$hlink}) {$dm}{$plink} {$dm}[{$size}]</s>";
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialShortpages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $spp = new ShortPagesPage();
+
+       return $spp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Specialpages.php b/includes/specials/Specialpages.php
new file mode 100644 (file)
index 0000000..ca91ad5
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialSpecialpages() {
+       global $wgOut, $wgUser, $wgMessageCache, $wgSortSpecialPages;
+
+       $wgMessageCache->loadAllMessages();
+
+       $wgOut->setRobotpolicy( 'noindex,nofollow' );  # Is this really needed?
+       $sk = $wgUser->getSkin();
+
+       $pages = SpecialPage::getUsablePages();
+
+       if( count( $pages ) == 0 ) {
+               # Yeah, that was pointless. Thanks for coming.
+               return;
+       }
+
+       /** Put them into a sortable array */
+       $groups = array();
+       foreach ( $pages as $page ) {
+               if ( $page->isListed() ) {
+                       $group = SpecialPage::getGroup( $page );
+                       if( !isset($groups[$group]) ) {
+                               $groups[$group] = array();
+                       }
+                       $groups[$group][$page->getDescription()] = array( $page->getTitle(), $page->isRestricted() );
+               }
+       }
+
+       /** Sort */
+       if ( $wgSortSpecialPages ) {
+               foreach( $groups as $group => $sortedPages ) {
+                       ksort( $groups[$group] );
+               }
+       }
+
+       /** Always move "other" to end */
+       if( array_key_exists('other',$groups) ) {
+               $other = $groups['other'];
+               unset( $groups['other'] );
+               $groups['other'] = $other;
+       }
+
+       /** Now output the HTML */
+       foreach ( $groups as $group => $sortedPages ) {
+               $middle = ceil( count($sortedPages)/2 );
+               $total = count($sortedPages);
+               $count = 0;
+
+               $wgOut->addHTML( "<h4 class='mw-specialpagesgroup'>".wfMsgHtml("specialpages-group-$group")."</h4>\n" );
+               $wgOut->addHTML( "<table style='width: 100%;' class='mw-specialpages-table'><tr>" );
+               $wgOut->addHTML( "<td width='30%' valign='top'><ul>\n" );
+               foreach( $sortedPages as $desc => $specialpage ) {
+                       list( $title, $restricted ) = $specialpage;
+                       $link = $sk->makeKnownLinkObj( $title , htmlspecialchars( $desc ) );
+                       if( $restricted ) {
+                               $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'>{$link}</li>\n" );
+                       } else {
+                               $wgOut->addHTML( "<li>{$link}</li>\n" );
+                       }
+
+                       # Split up the larger groups
+                       $count++;
+                       if( $total > 3 && $count == $middle ) {
+                               $wgOut->addHTML( "</ul></td><td width='10%'></td><td width='30%' valign='top'><ul>" );
+                       }
+               }
+               $wgOut->addHTML( "</ul></td><td width='30%' valign='top'></td></tr></table>\n" );
+       }
+       $wgOut->addHTML(
+               Xml::openElement('div', array( 'class' => 'mw-specialpages-notes' )).
+               wfMsgWikiHtml('specialpages-note').
+               Xml::closeElement('div')
+       );
+}
diff --git a/includes/specials/Statistics.php b/includes/specials/Statistics.php
new file mode 100644 (file)
index 0000000..570a21c
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Special page lists various statistics, including the contents of
+ * `site_stats`, plus page view details if enabled
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Show the special page
+ *
+ * @param mixed $par (not used)
+ */
+function wfSpecialStatistics( $par = '' ) {
+       global $wgOut, $wgLang, $wgRequest;
+       $dbr = wfGetDB( DB_SLAVE );
+
+       $views = SiteStats::views();
+       $edits = SiteStats::edits();
+       $good = SiteStats::articles();
+       $images = SiteStats::images();
+       $total = SiteStats::pages();
+       $users = SiteStats::users();
+       $admins = SiteStats::admins();
+       $numJobs = SiteStats::jobs();
+
+       if( $wgRequest->getVal( 'action' ) == 'raw' ) {
+               $wgOut->disable();
+               header( 'Pragma: nocache' );
+               echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n";
+               return;
+       } else {
+               $text = "__NOTOC__\n";
+               $text .= '==' . wfMsgNoTrans( 'sitestats' ) . "==\n";
+               $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ),
+                       $wgLang->formatNum( $total ),
+                       $wgLang->formatNum( $good ),
+                       $wgLang->formatNum( $views ),
+                       $wgLang->formatNum( $edits ),
+                       $wgLang->formatNum( sprintf( '%.2f', $total ? $edits / $total : 0 ) ),
+                       $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ),
+                       $wgLang->formatNum( $numJobs ),
+                       $wgLang->formatNum( $images )
+               )."\n";
+
+               $text .= "==" . wfMsgNoTrans( 'userstats' ) . "==\n";
+               $text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ),
+                       $wgLang->formatNum( $users ),
+                       $wgLang->formatNum( $admins ),
+                       '[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility
+                       $wgLang->formatNum( @sprintf( '%.2f', $admins / $users * 100 ) ),
+                       User::makeGroupLinkWiki( 'sysop' )
+               )."\n";
+
+               global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang;
+               if( !$wgDisableCounters && !$wgMiserMode ) {
+                       $res = $dbr->select(
+                               'page',
+                               array(
+                                       'page_namespace',
+                                       'page_title',
+                                       'page_counter',
+                               ),
+                               array(
+                                       'page_is_redirect' => 0,
+                                       'page_counter > 0',
+                               ),
+                               __METHOD__,
+                               array(
+                                       'ORDER BY' => 'page_counter DESC',
+                                       'LIMIT' => 10,
+                               )
+                       );
+                       if( $res->numRows() > 0 ) {
+                               $text .= "==" . wfMsgNoTrans( 'statistics-mostpopular' ) . "==\n";
+                               while( $row = $res->fetchObject() ) {
+                                       $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
+                                       if( $title instanceof Title )
+                                               $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n";
+                               }
+                               $res->free();
+                       }
+               }
+
+               $footer = wfMsgNoTrans( 'statistics-footer' );
+               if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' )
+                       $text .= "\n" . $footer;
+
+               $wgOut->addWikiText( $text );
+       }
+}
diff --git a/includes/specials/Uncategorizedcategories.php b/includes/specials/Uncategorizedcategories.php
new file mode 100644 (file)
index 0000000..f23e89c
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Uncategorizedcategories
+ * @ingroup SpecialPage
+ */
+class UncategorizedCategoriesPage extends UncategorizedPagesPage {
+       function UncategorizedCategoriesPage() {
+               $this->requestedNamespace = NS_CATEGORY;
+       }
+
+       function getName() {
+               return "Uncategorizedcategories";
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedcategories() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $lpp = new UncategorizedCategoriesPage();
+
+       return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Uncategorizedimages.php b/includes/specials/Uncategorizedimages.php
new file mode 100644 (file)
index 0000000..986ec96
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Special page lists images which haven't been categorised
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class UncategorizedImagesPage extends ImageQueryPage {
+
+       function getName() {
+               return 'Uncategorizedimages';
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function isExpensive() {
+               return true;
+       }
+
+       function isSyndicated() {
+               return false;
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+               $ns = NS_IMAGE;
+
+               return "SELECT 'Uncategorizedimages' AS type, page_namespace AS namespace,
+                               page_title AS title, page_title AS value
+                               FROM {$page} LEFT JOIN {$categorylinks} ON page_id = cl_from
+                               WHERE cl_from IS NULL AND page_namespace = {$ns} AND page_is_redirect = 0";
+       }
+
+}
+
+function wfSpecialUncategorizedimages() {
+       $uip = new UncategorizedImagesPage();
+       list( $limit, $offset ) = wfCheckLimits();
+       return $uip->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Uncategorizedpages.php b/includes/specials/Uncategorizedpages.php
new file mode 100644 (file)
index 0000000..e7f0aac
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page looking for page without any category.
+ * @ingroup SpecialPage
+ */
+class UncategorizedPagesPage extends PageQueryPage {
+       var $requestedNamespace = NS_MAIN;
+
+       function getName() {
+               return "Uncategorizedpages";
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function isExpensive() {
+               return true;
+       }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $categorylinks ) = $dbr->tableNamesN( 'page', 'categorylinks' );
+               $name = $dbr->addQuotes( $this->getName() );
+
+               return
+                       "
+                       SELECT
+                               $name as type,
+                               page_namespace AS namespace,
+                               page_title AS title,
+                               page_title AS value
+                       FROM $page
+                       LEFT JOIN $categorylinks ON page_id=cl_from
+                       WHERE cl_from IS NULL AND page_namespace={$this->requestedNamespace} AND page_is_redirect=0
+                       ";
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUncategorizedpages() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $lpp = new UncategorizedPagesPage();
+
+       return $lpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Uncategorizedtemplates.php b/includes/specials/Uncategorizedtemplates.php
new file mode 100644 (file)
index 0000000..cb2a6d4
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists all uncategorised pages in the
+ * template namespace
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class UncategorizedTemplatesPage extends UncategorizedPagesPage {
+
+       var $requestedNamespace = NS_TEMPLATE;
+
+       public function getName() {
+               return 'Uncategorizedtemplates';
+       }
+
+}
+
+/**
+ * Main execution point
+ *
+ * @param mixed $par Parameter passed to the page
+ */
+function wfSpecialUncategorizedtemplates() {
+       list( $limit, $offset ) = wfCheckLimits();
+       $utp = new UncategorizedTemplatesPage();
+       $utp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Undelete.php b/includes/specials/Undelete.php
new file mode 100644 (file)
index 0000000..33d9476
--- /dev/null
@@ -0,0 +1,1278 @@
+<?php
+
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and restore deleted content
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Constructor
+ */
+function wfSpecialUndelete( $par ) {
+       global $wgRequest;
+
+       $form = new UndeleteForm( $wgRequest, $par );
+       $form->execute();
+}
+
+/**
+ * Used to show archived pages and eventually restore them.
+ * @ingroup SpecialPage
+ */
+class PageArchive {
+       protected $title;
+       var $fileStatus;
+
+       function __construct( $title ) {
+               if( is_null( $title ) ) {
+                       throw new MWException( 'Archiver() given a null title.');
+               }
+               $this->title = $title;
+       }
+
+       /**
+        * List all deleted pages recorded in the archive table. Returns result
+        * wrapper with (ar_namespace, ar_title, count) fields, ordered by page
+        * namespace/title.
+        *
+        * @return ResultWrapper
+        */
+       public static function listAllPages() {
+               $dbr = wfGetDB( DB_SLAVE );
+               return self::listPages( $dbr, '' );
+       }
+
+       /**
+        * List deleted pages recorded in the archive table matching the
+        * given title prefix.
+        * Returns result wrapper with (ar_namespace, ar_title, count) fields.
+        *
+        * @return ResultWrapper
+        */
+       public static function listPagesByPrefix( $prefix ) {
+               $dbr = wfGetDB( DB_SLAVE );
+
+               $title = Title::newFromText( $prefix );
+               if( $title ) {
+                       $ns = $title->getNamespace();
+                       $encPrefix = $dbr->escapeLike( $title->getDBkey() );
+               } else {
+                       // Prolly won't work too good
+                       // @todo handle bare namespace names cleanly?
+                       $ns = 0;
+                       $encPrefix = $dbr->escapeLike( $prefix );
+               }
+               $conds = array(
+                       'ar_namespace' => $ns,
+                       "ar_title LIKE '$encPrefix%'",
+               );
+               return self::listPages( $dbr, $conds );
+       }
+
+       protected static function listPages( $dbr, $condition ) {
+               return $dbr->resultObject(
+                       $dbr->select(
+                               array( 'archive' ),
+                               array(
+                                       'ar_namespace',
+                                       'ar_title',
+                                       'COUNT(*) AS count'
+                               ),
+                               $condition,
+                               __METHOD__,
+                               array(
+                                       'GROUP BY' => 'ar_namespace,ar_title',
+                                       'ORDER BY' => 'ar_namespace,ar_title',
+                                       'LIMIT' => 100,
+                               )
+                       )
+               );
+       }
+
+       /**
+        * List the revisions of the given page. Returns result wrapper with
+        * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
+        *
+        * @return ResultWrapper
+        */
+       function listRevisions() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'archive',
+                       array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted' ),
+                       array( 'ar_namespace' => $this->title->getNamespace(),
+                              'ar_title' => $this->title->getDBkey() ),
+                       'PageArchive::listRevisions',
+                       array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+               $ret = $dbr->resultObject( $res );
+               return $ret;
+       }
+
+       /**
+        * List the deleted file revisions for this page, if it's a file page.
+        * Returns a result wrapper with various filearchive fields, or null
+        * if not a file page.
+        *
+        * @return ResultWrapper
+        * @todo Does this belong in Image for fuller encapsulation?
+        */
+       function listFiles() {
+               if( $this->title->getNamespace() == NS_IMAGE ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res = $dbr->select( 'filearchive',
+                               array(
+                                       'fa_id',
+                                       'fa_name',
+                                       'fa_archive_name',
+                                       'fa_storage_key',
+                                       'fa_storage_group',
+                                       'fa_size',
+                                       'fa_width',
+                                       'fa_height',
+                                       'fa_bits',
+                                       'fa_metadata',
+                                       'fa_media_type',
+                                       'fa_major_mime',
+                                       'fa_minor_mime',
+                                       'fa_description',
+                                       'fa_user',
+                                       'fa_user_text',
+                                       'fa_timestamp',
+                                       'fa_deleted' ),
+                               array( 'fa_name' => $this->title->getDBkey() ),
+                               __METHOD__,
+                               array( 'ORDER BY' => 'fa_timestamp DESC' ) );
+                       $ret = $dbr->resultObject( $res );
+                       return $ret;
+               }
+               return null;
+       }
+
+       /**
+        * Fetch (and decompress if necessary) the stored text for the deleted
+        * revision of the page with the given timestamp.
+        *
+        * @return string
+        * @deprecated Use getRevision() for more flexible information
+        */
+       function getRevisionText( $timestamp ) {
+               $rev = $this->getRevision( $timestamp );
+               return $rev ? $rev->getText() : null;
+       }
+
+       /**
+        * Return a Revision object containing data for the deleted revision.
+        * Note that the result *may* or *may not* have a null page ID.
+        * @param string $timestamp
+        * @return Revision
+        */
+       function getRevision( $timestamp ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               $row = $dbr->selectRow( 'archive',
+                       array(
+                               'ar_rev_id',
+                               'ar_text',
+                               'ar_comment',
+                               'ar_user',
+                               'ar_user_text',
+                               'ar_timestamp',
+                               'ar_minor_edit',
+                               'ar_flags',
+                               'ar_text_id',
+                               'ar_deleted',
+                               'ar_len' ),
+                       array( 'ar_namespace' => $this->title->getNamespace(),
+                              'ar_title' => $this->title->getDBkey(),
+                              'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
+                       __METHOD__ );
+               if( $row ) {
+                       return new Revision( array(
+                               'page'       => $this->title->getArticleId(),
+                               'id'         => $row->ar_rev_id,
+                               'text'       => ($row->ar_text_id
+                                       ? null
+                                       : Revision::getRevisionText( $row, 'ar_' ) ),
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'text_id'    => $row->ar_text_id,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len) );
+               } else {
+                       return null;
+               }
+       }
+
+       /**
+        * Return the most-previous revision, either live or deleted, against
+        * the deleted revision given by timestamp.
+        *
+        * May produce unexpected results in case of history merges or other
+        * unusual time issues.
+        *
+        * @param string $timestamp
+        * @return Revision or null
+        */
+       function getPreviousRevision( $timestamp ) {
+               $dbr = wfGetDB( DB_SLAVE );
+
+               // Check the previous deleted revision...
+               $row = $dbr->selectRow( 'archive',
+                       'ar_timestamp',
+                       array( 'ar_namespace' => $this->title->getNamespace(),
+                              'ar_title' => $this->title->getDBkey(),
+                              'ar_timestamp < ' .
+                                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+                       __METHOD__,
+                       array(
+                               'ORDER BY' => 'ar_timestamp DESC',
+                               'LIMIT' => 1 ) );
+               $prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+
+               $row = $dbr->selectRow( array( 'page', 'revision' ),
+                       array( 'rev_id', 'rev_timestamp' ),
+                       array(
+                               'page_namespace' => $this->title->getNamespace(),
+                               'page_title' => $this->title->getDBkey(),
+                               'page_id = rev_page',
+                               'rev_timestamp < ' .
+                                               $dbr->addQuotes( $dbr->timestamp( $timestamp ) ) ),
+                       __METHOD__,
+                       array(
+                               'ORDER BY' => 'rev_timestamp DESC',
+                               'LIMIT' => 1 ) );
+               $prevLive = $row ? wfTimestamp( TS_MW, $row->rev_timestamp ) : false;
+               $prevLiveId = $row ? intval( $row->rev_id ) : null;
+
+               if( $prevLive && $prevLive > $prevDeleted ) {
+                       // Most prior revision was live
+                       return Revision::newFromId( $prevLiveId );
+               } elseif( $prevDeleted ) {
+                       // Most prior revision was deleted
+                       return $this->getRevision( $prevDeleted );
+               } else {
+                       // No prior revision on this page.
+                       return null;
+               }
+       }
+
+       /**
+        * Get the text from an archive row containing ar_text, ar_flags and ar_text_id
+        */
+       function getTextFromRow( $row ) {
+               if( is_null( $row->ar_text_id ) ) {
+                       // An old row from MediaWiki 1.4 or previous.
+                       // Text is embedded in this row in classic compression format.
+                       return Revision::getRevisionText( $row, "ar_" );
+               } else {
+                       // New-style: keyed to the text storage backend.
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $text = $dbr->selectRow( 'text',
+                               array( 'old_text', 'old_flags' ),
+                               array( 'old_id' => $row->ar_text_id ),
+                               __METHOD__ );
+                       return Revision::getRevisionText( $text );
+               }
+       }
+
+
+       /**
+        * Fetch (and decompress if necessary) the stored text of the most
+        * recently edited deleted revision of the page.
+        *
+        * If there are no archived revisions for the page, returns NULL.
+        *
+        * @return string
+        */
+       function getLastRevisionText() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $row = $dbr->selectRow( 'archive',
+                       array( 'ar_text', 'ar_flags', 'ar_text_id' ),
+                       array( 'ar_namespace' => $this->title->getNamespace(),
+                              'ar_title' => $this->title->getDBkey() ),
+                       'PageArchive::getLastRevisionText',
+                       array( 'ORDER BY' => 'ar_timestamp DESC' ) );
+               if( $row ) {
+                       return $this->getTextFromRow( $row );
+               } else {
+                       return NULL;
+               }
+       }
+
+       /**
+        * Quick check if any archived revisions are present for the page.
+        * @return bool
+        */
+       function isDeleted() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+                       array( 'ar_namespace' => $this->title->getNamespace(),
+                              'ar_title' => $this->title->getDBkey() ) );
+               return ($n > 0);
+       }
+
+       /**
+        * Restore the given (or all) text and file revisions for the page.
+        * Once restored, the items will be removed from the archive tables.
+        * The deletion log will be updated with an undeletion notice.
+        *
+        * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+        * @param string $comment
+        * @param array $fileVersions
+        * @param bool $unsuppress
+        *
+        * @return array(number of file revisions restored, number of image revisions restored, log message)
+        * on success, false on failure
+        */
+       function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false ) {
+               // If both the set of text revisions and file revisions are empty,
+               // restore everything. Otherwise, just restore the requested items.
+               $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+
+               $restoreText = $restoreAll || !empty( $timestamps );
+               $restoreFiles = $restoreAll || !empty( $fileVersions );
+
+               if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
+                       $img = wfLocalFile( $this->title );
+                       $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
+                       $filesRestored = $this->fileStatus->successCount;
+               } else {
+                       $filesRestored = 0;
+               }
+
+               if( $restoreText ) {
+                       $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress );
+                       if($textRestored === false) // It must be one of UNDELETE_*
+                               return false;
+               } else {
+                       $textRestored = 0;
+               }
+
+               // Touch the log!
+               global $wgContLang;
+               $log = new LogPage( 'delete' );
+
+               if( $textRestored && $filesRestored ) {
+                       $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ),
+                               $wgContLang->formatNum( $textRestored ),
+                               $wgContLang->formatNum( $filesRestored ) );
+               } elseif( $textRestored ) {
+                       $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ),
+                               $wgContLang->formatNum( $textRestored ) );
+               } elseif( $filesRestored ) {
+                       $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ),
+                               $wgContLang->formatNum( $filesRestored ) );
+               } else {
+                       wfDebug( "Undelete: nothing undeleted...\n" );
+                       return false;
+               }
+
+               if( trim( $comment ) != '' )
+                       $reason .= ": {$comment}";
+               $log->addEntry( 'restore', $this->title, $reason );
+
+               return array($textRestored, $filesRestored, $reason);
+       }
+
+       /**
+        * This is the meaty bit -- restores archived revisions of the given page
+        * to the cur/old tables. If the page currently exists, all revisions will
+        * be stuffed into old, otherwise the most recent will go into cur.
+        *
+        * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+        * @param string $comment
+        * @param array $fileVersions
+        * @param bool $unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
+        *
+        * @return mixed number of revisions restored or false on failure
+        */
+       private function undeleteRevisions( $timestamps, $unsuppress = false ) {
+               if ( wfReadOnly() )
+                       return false;
+               $restoreAll = empty( $timestamps );
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               # Does this page already exist? We'll have to update it...
+               $article = new Article( $this->title );
+               $options = 'FOR UPDATE';
+               $page = $dbw->selectRow( 'page',
+                       array( 'page_id', 'page_latest' ),
+                       array( 'page_namespace' => $this->title->getNamespace(),
+                              'page_title'     => $this->title->getDBkey() ),
+                       __METHOD__,
+                       $options );
+               if( $page ) {
+                       $makepage = false;
+                       # Page already exists. Import the history, and if necessary
+                       # we'll update the latest revision field in the record.
+                       $newid             = 0;
+                       $pageId            = $page->page_id;
+                       $previousRevId     = $page->page_latest;
+                       # Get the time span of this page
+                       $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+                               array( 'rev_id' => $previousRevId ),
+                               __METHOD__ );
+                       if( $previousTimestamp === false ) {
+                               wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
+                               return 0;
+                       }
+               } else {
+                       # Have to create a new article...
+                       $makepage = true;
+                       $previousRevId = 0;
+                       $previousTimestamp = 0;
+               }
+
+               if( $restoreAll ) {
+                       $oldones = '1 = 1'; # All revisions...
+               } else {
+                       $oldts = implode( ',',
+                               array_map( array( &$dbw, 'addQuotes' ),
+                                       array_map( array( &$dbw, 'timestamp' ),
+                                               $timestamps ) ) );
+
+                       $oldones = "ar_timestamp IN ( {$oldts} )";
+               }
+
+               /**
+                * Select each archived revision...
+                */
+               $result = $dbw->select( 'archive',
+                       /* fields */ array(
+                               'ar_rev_id',
+                               'ar_text',
+                               'ar_comment',
+                               'ar_user',
+                               'ar_user_text',
+                               'ar_timestamp',
+                               'ar_minor_edit',
+                               'ar_flags',
+                               'ar_text_id',
+                               'ar_deleted',
+                               'ar_page_id',
+                               'ar_len' ),
+                       /* WHERE */ array(
+                               'ar_namespace' => $this->title->getNamespace(),
+                               'ar_title'     => $this->title->getDBkey(),
+                               $oldones ),
+                       __METHOD__,
+                       /* options */ array(
+                               'ORDER BY' => 'ar_timestamp' )
+                       );
+               $ret = $dbw->resultObject( $result );
+
+               $rev_count = $dbw->numRows( $result );
+               if( $rev_count ) {
+                       # We need to seek around as just using DESC in the ORDER BY
+                       # would leave the revisions inserted in the wrong order
+                       $first = $ret->fetchObject();
+                       $ret->seek( $rev_count - 1 );
+                       $last = $ret->fetchObject();
+                       // We don't handle well changing the top revision's settings
+                       if( !$unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
+                               wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
+                               return false;
+                       }
+                       $ret->seek( 0 );
+               }
+
+               if( $makepage ) {
+                       $newid  = $article->insertOn( $dbw );
+                       $pageId = $newid;
+               }
+
+               $revision = null;
+               $restored = 0;
+
+               while( $row = $ret->fetchObject() ) {
+                       if( $row->ar_text_id ) {
+                               // Revision was deleted in 1.5+; text is in
+                               // the regular text table, use the reference.
+                               // Specify null here so the so the text is
+                               // dereferenced for page length info if needed.
+                               $revText = null;
+                       } else {
+                               // Revision was deleted in 1.4 or earlier.
+                               // Text is squashed into the archive row, and
+                               // a new text table entry will be created for it.
+                               $revText = Revision::getRevisionText( $row, 'ar_' );
+                       }
+                       $revision = new Revision( array(
+                               'page'       => $pageId,
+                               'id'         => $row->ar_rev_id,
+                               'text'       => $revText,
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'text_id'    => $row->ar_text_id,
+                               'deleted'        => $unsuppress ? 0 : $row->ar_deleted,
+                               'len'        => $row->ar_len
+                               ) );
+                       $revision->insertOn( $dbw );
+                       $restored++;
+
+                       wfRunHooks( 'ArticleRevisionUndeleted', array( &$this->title, $revision, $row->ar_page_id ) );
+               }
+               // Was anything restored at all?
+               if($restored == 0)
+                       return 0;
+
+               if( $revision ) {
+                       // Attach the latest revision to the page...
+                       $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
+
+                       if( $newid || $wasnew ) {
+                               // Update site stats, link tables, etc
+                               $article->createUpdates( $revision );
+                       }
+
+                       if( $newid ) {
+                               wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
+                               Article::onArticleCreate( $this->title );
+                       } else {
+                               wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
+                               Article::onArticleEdit( $this->title );
+                       }
+
+                       if( $this->title->getNamespace() == NS_IMAGE ) {
+                               $update = new HTMLCacheUpdate( $this->title, 'imagelinks' );
+                               $update->doUpdate();
+                       }
+               } else {
+                       // Revision couldn't be created. This is very weird
+                       return self::UNDELETE_UNKNOWNERR;
+               }
+
+               # Now that it's safely stored, take it out of the archive
+               $dbw->delete( 'archive',
+                       /* WHERE */ array(
+                               'ar_namespace' => $this->title->getNamespace(),
+                               'ar_title' => $this->title->getDBkey(),
+                               $oldones ),
+                       __METHOD__ );
+
+               return $restored;
+       }
+
+       function getFileStatus() { return $this->fileStatus; }
+}
+
+/**
+ * The HTML form for Special:Undelete, which allows users with the appropriate
+ * permissions to view and restore deleted content.
+ * @ingroup SpecialPage
+ */
+class UndeleteForm {
+       var $mAction, $mTarget, $mTimestamp, $mRestore, $mTargetObj;
+       var $mTargetTimestamp, $mAllowed, $mComment;
+
+       function UndeleteForm( $request, $par = "" ) {
+               global $wgUser;
+               $this->mAction = $request->getVal( 'action' );
+               $this->mTarget = $request->getVal( 'target' );
+               $this->mSearchPrefix = $request->getText( 'prefix' );
+               $time = $request->getVal( 'timestamp' );
+               $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
+               $this->mFile = $request->getVal( 'file' );
+
+               $posted = $request->wasPosted() &&
+                       $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+               $this->mRestore = $request->getCheck( 'restore' ) && $posted;
+               $this->mPreview = $request->getCheck( 'preview' ) && $posted;
+               $this->mDiff = $request->getCheck( 'diff' );
+               $this->mComment = $request->getText( 'wpComment' );
+               $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
+
+               if( $par != "" ) {
+                       $this->mTarget = $par;
+               }
+               if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
+                       $this->mAllowed = true;
+               } else {
+                       $this->mAllowed = false;
+                       $this->mTimestamp = '';
+                       $this->mRestore = false;
+               }
+               if ( $this->mTarget !== "" ) {
+                       $this->mTargetObj = Title::newFromURL( $this->mTarget );
+               } else {
+                       $this->mTargetObj = NULL;
+               }
+               if( $this->mRestore ) {
+                       $timestamps = array();
+                       $this->mFileVersions = array();
+                       foreach( $_REQUEST as $key => $val ) {
+                               $matches = array();
+                               if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
+                                       array_push( $timestamps, $matches[1] );
+                               }
+
+                               if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
+                                       $this->mFileVersions[] = intval( $matches[1] );
+                               }
+                       }
+                       rsort( $timestamps );
+                       $this->mTargetTimestamp = $timestamps;
+               }
+       }
+
+       function execute() {
+               global $wgOut, $wgUser;
+               if ( $this->mAllowed ) {
+                       $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+               } else {
+                       $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
+               }
+
+               if( is_null( $this->mTargetObj ) ) {
+               # Not all users can just browse every deleted page from the list
+                       if( $wgUser->isAllowed( 'browsearchive' ) ) {
+                               $this->showSearchForm();
+
+                               # List undeletable articles
+                               if( $this->mSearchPrefix ) {
+                                       $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
+                                       $this->showList( $result );
+                               }
+                       } else {
+                               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
+                       }
+                       return;
+               }
+               if( $this->mTimestamp !== '' ) {
+                       return $this->showRevision( $this->mTimestamp );
+               }
+               if( $this->mFile !== null ) {
+                       $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
+                       // Check if user is allowed to see this file
+                       if( !$file->userCan( File::DELETED_FILE ) ) {
+                               $wgOut->permissionRequired( 'suppressrevision' );
+                               return false;
+                       } else {
+                               return $this->showFile( $this->mFile );
+                       }
+               }
+               if( $this->mRestore && $this->mAction == "submit" ) {
+                       return $this->undelete();
+               }
+               return $this->showHistory();
+       }
+
+       function showSearchForm() {
+               global $wgOut, $wgScript;
+               $wgOut->addWikiMsg( 'undelete-header' );
+
+               $wgOut->addHtml(
+                       Xml::openElement( 'form', array(
+                               'method' => 'get',
+                               'action' => $wgScript ) ) .
+                       '<fieldset>' .
+                       Xml::element( 'legend', array(),
+                               wfMsg( 'undelete-search-box' ) ) .
+                       Xml::hidden( 'title',
+                               SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) .
+                       Xml::inputLabel( wfMsg( 'undelete-search-prefix' ),
+                               'prefix', 'prefix', 20,
+                               $this->mSearchPrefix ) .
+                       Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) .
+                       '</fieldset>' .
+                       '</form>' );
+       }
+
+       // Generic list of deleted pages
+       private function showList( $result ) {
+               global $wgLang, $wgContLang, $wgUser, $wgOut;
+
+               if( $result->numRows() == 0 ) {
+                       $wgOut->addWikiMsg( 'undelete-no-results' );
+                       return;
+               }
+
+               $wgOut->addWikiMsg( "undeletepagetext" );
+
+               $sk = $wgUser->getSkin();
+               $undelete = SpecialPage::getTitleFor( 'Undelete' );
+               $wgOut->addHTML( "<ul>\n" );
+               while( $row = $result->fetchObject() ) {
+                       $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
+                       $link = $sk->makeKnownLinkObj( $undelete, htmlspecialchars( $title->getPrefixedText() ),
+                               'target=' . $title->getPrefixedUrl() );
+                       #$revs = wfMsgHtml( 'undeleterevisions', $wgLang->formatNum( $row->count ) );
+                       $revs = wfMsgExt( 'undeleterevisions',
+                               array( 'parseinline' ),
+                               $wgLang->formatNum( $row->count ) );
+                       $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" );
+               }
+               $result->free();
+               $wgOut->addHTML( "</ul>\n" );
+
+               return true;
+       }
+
+       private function showRevision( $timestamp ) {
+               global $wgLang, $wgUser, $wgOut;
+               $self = SpecialPage::getTitleFor( 'Undelete' );
+               $skin = $wgUser->getSkin();
+
+               if(!preg_match("/[0-9]{14}/",$timestamp)) return 0;
+
+               $archive = new PageArchive( $this->mTargetObj );
+               $rev = $archive->getRevision( $timestamp );
+
+               if( !$rev ) {
+                       $wgOut->addWikiMsg( 'undeleterevision-missing' );
+                       return;
+               }
+
+               if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+                       if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                               $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+                               return;
+                       } else {
+                               $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+                               $wgOut->addHTML( '<br/>' );
+                               // and we are allowed to see...
+                       }
+               }
+
+               $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
+
+               $link = $skin->makeKnownLinkObj(
+                       SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
+                       htmlspecialchars( $this->mTargetObj->getPrefixedText() )
+               );
+               $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
+               $user = $skin->revUserTools( $rev );
+
+               if( $this->mDiff ) {
+                       $previousRev = $archive->getPreviousRevision( $timestamp );
+                       if( $previousRev ) {
+                               $this->showDiff( $previousRev, $rev );
+                               if( $wgUser->getOption( 'diffonly' ) ) {
+                                       return;
+                               } else {
+                                       $wgOut->addHtml( '<hr />' );
+                               }
+                       } else {
+                               $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) );
+                       }
+               }
+
+               $wgOut->addHtml( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '</p>' );
+
+               wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
+
+               if( $this->mPreview ) {
+                       $wgOut->addHtml( "<hr />\n" );
+
+                       //Hide [edit]s
+                       $popts = $wgOut->parserOptions();
+                       $popts->setEditSection( false );
+                       $wgOut->parserOptions( $popts );
+                       $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, true );
+               }
+
+               $wgOut->addHtml(
+                       wfElement( 'textarea', array(
+                                       'readonly' => 'readonly',
+                                       'cols' => intval( $wgUser->getOption( 'cols' ) ),
+                                       'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
+                               $rev->revText() . "\n" ) .
+                       wfOpenElement( 'div' ) .
+                       wfOpenElement( 'form', array(
+                               'method' => 'post',
+                               'action' => $self->getLocalURL( "action=submit" ) ) ) .
+                       wfElement( 'input', array(
+                               'type' => 'hidden',
+                               'name' => 'target',
+                               'value' => $this->mTargetObj->getPrefixedDbKey() ) ) .
+                       wfElement( 'input', array(
+                               'type' => 'hidden',
+                               'name' => 'timestamp',
+                               'value' => $timestamp ) ) .
+                       wfElement( 'input', array(
+                               'type' => 'hidden',
+                               'name' => 'wpEditToken',
+                               'value' => $wgUser->editToken() ) ) .
+                       wfElement( 'input', array(
+                               'type' => 'submit',
+                               'name' => 'preview',
+                               'value' => wfMsg( 'showpreview' ) ) ) .
+                       wfElement( 'input', array(
+                               'name' => 'diff',
+                               'type' => 'submit',
+                               'value' => wfMsg( 'showdiff' ) ) ) .
+                       wfCloseElement( 'form' ) .
+                       wfCloseElement( 'div' ) );
+       }
+
+       /**
+        * Build a diff display between this and the previous either deleted
+        * or non-deleted edit.
+        * @param Revision $previousRev
+        * @param Revision $currentRev
+        * @return string HTML
+        */
+       function showDiff( $previousRev, $currentRev ) {
+               global $wgOut, $wgUser;
+
+               $diffEngine = new DifferenceEngine();
+               $diffEngine->showDiffStyle();
+               $wgOut->addHtml(
+                       "<div>" .
+                       "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+                       "<col class='diff-marker' />" .
+                       "<col class='diff-content' />" .
+                       "<col class='diff-marker' />" .
+                       "<col class='diff-content' />" .
+                       "<tr>" .
+                               "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
+                               $this->diffHeader( $previousRev ) .
+                               "</td>" .
+                               "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
+                               $this->diffHeader( $currentRev ) .
+                               "</td>" .
+                       "</tr>" .
+                       $diffEngine->generateDiffBody(
+                               $previousRev->getText(), $currentRev->getText() ) .
+                       "</table>" .
+                       "</div>\n" );
+
+       }
+
+       private function diffHeader( $rev ) {
+               global $wgUser, $wgLang, $wgLang;
+               $sk = $wgUser->getSkin();
+               $isDeleted = !( $rev->getId() && $rev->getTitle() );
+               if( $isDeleted ) {
+                       /// @fixme $rev->getTitle() is null for deleted revs...?
+                       $targetPage = SpecialPage::getTitleFor( 'Undelete' );
+                       $targetQuery = 'target=' .
+                               $this->mTargetObj->getPrefixedUrl() .
+                               '&timestamp=' .
+                               wfTimestamp( TS_MW, $rev->getTimestamp() );
+               } else {
+                       /// @fixme getId() may return non-zero for deleted revs...
+                       $targetPage = $rev->getTitle();
+                       $targetQuery = 'oldid=' . $rev->getId();
+               }
+               return
+                       '<div id="mw-diff-otitle1"><strong>' .
+                               $sk->makeLinkObj( $targetPage,
+                                       wfMsgHtml( 'revisionasof',
+                                               $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
+                                       $targetQuery ) .
+                               ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
+                       '</strong></div>' .
+                       '<div id="mw-diff-otitle2">' .
+                               $sk->revUserTools( $rev ) . '<br/>' .
+                       '</div>' .
+                       '<div id="mw-diff-otitle3">' .
+                               $sk->revComment( $rev ) . '<br/>' .
+                       '</div>';
+       }
+
+       /**
+        * Show a deleted file version requested by the visitor.
+        */
+       private function showFile( $key ) {
+               global $wgOut, $wgRequest;
+               $wgOut->disable();
+
+               # We mustn't allow the output to be Squid cached, otherwise
+               # if an admin previews a deleted image, and it's cached, then
+               # a user without appropriate permissions can toddle off and
+               # nab the image, and Squid will serve it
+               $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+               $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
+               $wgRequest->response()->header( 'Pragma: no-cache' );
+
+               $store = FileStore::get( 'deleted' );
+               $store->stream( $key );
+       }
+
+       private function showHistory() {
+               global $wgLang, $wgUser, $wgOut;
+
+               $sk = $wgUser->getSkin();
+               if( $this->mAllowed ) {
+                       $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+               } else {
+                       $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
+               }
+
+               $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
+
+               $archive = new PageArchive( $this->mTargetObj );
+               /*
+               $text = $archive->getLastRevisionText();
+               if( is_null( $text ) ) {
+                       $wgOut->addWikiMsg( "nohistory" );
+                       return;
+               }
+               */
+               if ( $this->mAllowed ) {
+                       $wgOut->addWikiMsg( "undeletehistory" );
+                       $wgOut->addWikiMsg( "undeleterevdel" );
+               } else {
+                       $wgOut->addWikiMsg( "undeletehistorynoadmin" );
+               }
+
+               # List all stored revisions
+               $revisions = $archive->listRevisions();
+               $files = $archive->listFiles();
+
+               $haveRevisions = $revisions && $revisions->numRows() > 0;
+               $haveFiles = $files && $files->numRows() > 0;
+
+               # Batch existence check on user and talk pages
+               if( $haveRevisions ) {
+                       $batch = new LinkBatch();
+                       while( $row = $revisions->fetchObject() ) {
+                               $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
+                               $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
+                       }
+                       $batch->execute();
+                       $revisions->seek( 0 );
+               }
+               if( $haveFiles ) {
+                       $batch = new LinkBatch();
+                       while( $row = $files->fetchObject() ) {
+                               $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) );
+                               $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) );
+                       }
+                       $batch->execute();
+                       $files->seek( 0 );
+               }
+
+               if ( $this->mAllowed ) {
+                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
+                       $action = $titleObj->getLocalURL( "action=submit" );
+                       # Start the form here
+                       $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
+                       $wgOut->addHtml( $top );
+               }
+
+               # Show relevant lines from the deletion log:
+               $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" );
+               LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTargetObj->getPrefixedText() );
+
+               if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
+                       # Format the user-visible controls (comment field, submission button)
+                       # in a nice little table
+                       if( $wgUser->isAllowed( 'suppressrevision' ) ) {
+                               $unsuppressBox =
+                                       "<tr>
+                                               <td>&nbsp;</td>
+                                               <td class='mw-input'>" .
+                                                       Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress',
+                                                               'mw-undelete-unsuppress', $this->mUnsuppress ).
+                                               "</td>
+                                       </tr>";
+                       } else {
+                               $unsuppressBox = "";
+                       }
+                       $table =
+                               Xml::openElement( 'fieldset' ) .
+                               Xml::element( 'legend', null, wfMsg( 'undelete') ).
+                               Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
+                                       "<tr>
+                                               <td colspan='2'>" .
+                                                       wfMsgWikiHtml( 'undeleteextrahelp' ) .
+                                               "</td>
+                                       </tr>
+                                       <tr>
+                                               <td class='mw-label'>" .
+                                                       Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+                                               "</td>
+                                               <td class='mw-input'>" .
+                                                       Xml::input( 'wpComment', 50, $this->mComment, array( 'id' =>  'wpComment' ) ) .
+                                               "</td>
+                                       </tr>
+                                       <tr>
+                                               <td>&nbsp;</td>
+                                               <td class='mw-submit'>" .
+                                                       Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
+                                                       Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
+                                               "</td>
+                                       </tr>" .
+                                       $unsuppressBox .
+                               Xml::closeElement( 'table' ) .
+                               Xml::closeElement( 'fieldset' );
+
+                       $wgOut->addHtml( $table );
+               }
+
+               $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" );
+
+               if( $haveRevisions ) {
+                       # The page's stored (deleted) history:
+                       $wgOut->addHTML("<ul>");
+                       $target = urlencode( $this->mTarget );
+                       $remaining = $revisions->numRows();
+                       $earliestLiveTime = $this->getEarliestTime( $this->mTargetObj );
+
+                       while( $row = $revisions->fetchObject() ) {
+                               $remaining--;
+                               $wgOut->addHTML( $this->formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) );
+                       }
+                       $revisions->free();
+                       $wgOut->addHTML("</ul>");
+               } else {
+                       $wgOut->addWikiMsg( "nohistory" );
+               }
+
+               if( $haveFiles ) {
+                       $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
+                       $wgOut->addHtml( "<ul>" );
+                       while( $row = $files->fetchObject() ) {
+                               $wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
+                       }
+                       $files->free();
+                       $wgOut->addHTML( "</ul>" );
+               }
+
+               if ( $this->mAllowed ) {
+                       # Slip in the hidden controls here
+                       $misc  = Xml::hidden( 'target', $this->mTarget );
+                       $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
+                       $misc .= Xml::closeElement( 'form' );
+                       $wgOut->addHtml( $misc );
+               }
+
+               return true;
+       }
+
+       private function formatRevisionRow( $row, $earliestLiveTime, $remaining, $sk ) {
+               global $wgUser, $wgLang;
+
+               $rev = new Revision( array(
+                               'page'       => $this->mTargetObj->getArticleId(),
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len ) );
+
+               $stxt = '';
+               $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+               if( $this->mAllowed ) {
+                       $checkBox = Xml::check( "ts$ts" );
+                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
+                       $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
+                       # Last link
+                       if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+                               $last = wfMsgHtml('diff');
+                       } else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) {
+                               $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'),
+                                       "target=" . $this->mTargetObj->getPrefixedUrl() . "&timestamp=$ts&diff=prev" );
+                       } else {
+                               $last = wfMsgHtml('diff');
+                       }
+               } else {
+                       $checkBox = '';
+                       $pageLink = $wgLang->timeanddate( $ts, true );
+                       $last = wfMsgHtml('diff');
+               }
+               $userLink = $sk->revUserTools( $rev );
+
+               if(!is_null($size = $row->ar_len)) {
+                       if($size == 0)
+                               $stxt = wfMsgHtml('historyempty');
+                       else
+                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
+               }
+               $comment = $sk->revComment( $rev );
+               $revdlink = '';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $del = wfMsgHtml('rev-delundel');
+                       } else {
+                               $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+                               $del = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . $this->mTargetObj->getPrefixedUrl() . "&artimestamp=$ts" );
+                               // Bolden oversighted content
+                               if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $del = "<strong>$del</strong>";
+                       }
+                       $revdlink = "<tt>(<small>$del</small>)</tt>";
+               }
+
+               return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+       }
+
+       private function formatFileRow( $row, $sk ) {
+               global $wgUser, $wgLang;
+
+               $file = ArchivedFile::newFromRow( $row );
+
+               $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
+               if( $this->mAllowed && $row->fa_storage_key ) {
+                       $checkBox = Xml::check( "fileid" . $row->fa_id );
+                       $key = urlencode( $row->fa_storage_key );
+                       $target = urlencode( $this->mTarget );
+                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
+                       $pageLink = $this->getFileLink( $file, $titleObj, $ts, $key, $sk );
+               } else {
+                       $checkBox = '';
+                       $pageLink = $wgLang->timeanddate( $ts, true );
+               }
+               $userLink = $this->getFileUser( $file, $sk );
+               $data =
+                       wfMsgHtml( 'widthheight',
+                               $wgLang->formatNum( $row->fa_width ),
+                               $wgLang->formatNum( $row->fa_height ) ) .
+                       ' (' .
+                       wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
+                       ')';
+               $comment = $this->getFileComment( $file, $sk );
+               $revdlink = '';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $del = wfMsgHtml('rev-delundel');
+                       } else {
+                               $del = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . $this->mTargetObj->getPrefixedUrl() .
+                                       '&fileid=' . $row->fa_id );
+                               // Bolden oversighted content
+                               if( $file->isDeleted( File::DELETED_RESTRICTED ) )
+                                       $del = "<strong>$del</strong>";
+                       }
+                       $revdlink = "<tt>(<small>$del</small>)</tt>";
+               }
+               return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
+       }
+
+       private function getEarliestTime( $title ) {
+               $dbr = wfGetDB( DB_SLAVE );
+               if( $title->exists() ) {
+                       $min = $dbr->selectField( 'revision',
+                               'MIN(rev_timestamp)',
+                               array( 'rev_page' => $title->getArticleId() ),
+                               __METHOD__ );
+                       return wfTimestampOrNull( TS_MW, $min );
+               }
+               return null;
+       }
+
+       /**
+        * Fetch revision text link if it's available to all users
+        * @return string
+        */
+       function getPageLink( $rev, $titleObj, $ts, $sk ) {
+               global $wgLang;
+
+               if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
+                               "target=".$this->mTargetObj->getPrefixedUrl()."&timestamp=$ts" );
+                       if( $rev->isDeleted(Revision::DELETED_TEXT) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       /**
+        * Fetch image view link if it's available to all users
+        * @return string
+        */
+       function getFileLink( $file, $titleObj, $ts, $key, $sk ) {
+               global $wgLang;
+
+               if( !$file->userCan(File::DELETED_FILE) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
+                               "target=".$this->mTargetObj->getPrefixedUrl()."&file=$key" );
+                       if( $file->isDeleted(File::DELETED_FILE) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       /**
+        * Fetch file's user id if it's available to this user
+        * @return string
+        */
+       function getFileUser( $file, $sk ) {
+               if( !$file->userCan(File::DELETED_USER) ) {
+                       return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+               } else {
+                       $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) .
+                               $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() );
+                       if( $file->isDeleted(File::DELETED_USER) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       /**
+        * Fetch file upload comment if it's available to this user
+        * @return string
+        */
+       function getFileComment( $file, $sk ) {
+               if( !$file->userCan(File::DELETED_COMMENT) ) {
+                       return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
+               } else {
+                       $link = $sk->commentBlock( $file->getRawDescription() );
+                       if( $file->isDeleted(File::DELETED_COMMENT) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       function undelete() {
+               global $wgOut, $wgUser;
+               if ( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+               if( !is_null( $this->mTargetObj ) ) {
+                       $archive = new PageArchive( $this->mTargetObj );
+                       $ok = $archive->undelete(
+                               $this->mTargetTimestamp,
+                               $this->mComment,
+                               $this->mFileVersions,
+                               $this->mUnsuppress );
+
+                       if( is_array($ok) ) {
+                               if ( $ok[1] ) // Undeleted file count
+                                       wfRunHooks( 'FileUndeleteComplete', array(
+                                               $this->mTargetObj, $this->mFileVersions,
+                                               $wgUser, $this->mComment) );
+
+                               $skin = $wgUser->getSkin();
+                               $link = $skin->makeKnownLinkObj( $this->mTargetObj );
+                               $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
+                       } else {
+                               $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+                               $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
+                       }
+
+                       // Show file deletion warnings and errors
+                       $status = $archive->getFileStatus();
+                       if( $status && !$status->isGood() ) {
+                               $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
+                       }
+               } else {
+                       $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+               }
+               return false;
+       }
+}
diff --git a/includes/specials/Unlockdb.php b/includes/specials/Unlockdb.php
new file mode 100644 (file)
index 0000000..0bf7e5a
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ *
+ */
+function wfSpecialUnlockdb() {
+       global $wgUser, $wgOut, $wgRequest;
+
+       if( !$wgUser->isAllowed( 'siteadmin' ) ) {
+               $wgOut->permissionRequired( 'siteadmin' );
+               return;
+       }
+
+       $action = $wgRequest->getVal( 'action' );
+       $f = new DBUnlockForm();
+
+       if ( "success" == $action ) {
+               $f->showSuccess();
+       } else if ( "submit" == $action && $wgRequest->wasPosted() &&
+               $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
+               $f->doSubmit();
+       } else {
+               $f->showForm( "" );
+       }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class DBUnlockForm {
+       function showForm( $err )
+       {
+               global $wgOut, $wgUser;
+
+               global $wgReadOnlyFile;
+               if( !file_exists( $wgReadOnlyFile ) ) {
+                       $wgOut->addWikiMsg( 'databasenotlocked' );
+                       return;
+               }
+
+               $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+               $wgOut->addWikiMsg( "unlockdbtext" );
+
+               if ( "" != $err ) {
+                       $wgOut->setSubtitle( wfMsg( "formerror" ) );
+                       $wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
+               }
+               $lc = htmlspecialchars( wfMsg( "unlockconfirm" ) );
+               $lb = htmlspecialchars( wfMsg( "unlockbtn" ) );
+               $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
+               $action = $titleObj->escapeLocalURL( "action=submit" );
+               $token = htmlspecialchars( $wgUser->editToken() );
+
+               $wgOut->addHTML( <<<END
+
+<form id="unlockdb" method="post" action="{$action}">
+<table border="0">
+       <tr>
+               <td align="right">
+                       <input type="checkbox" name="wpLockConfirm" />
+               </td>
+               <td align="left">{$lc}</td>
+       </tr>
+       <tr>
+               <td>&nbsp;</td>
+               <td align="left">
+                       <input type="submit" name="wpLock" value="{$lb}" />
+               </td>
+       </tr>
+</table>
+<input type="hidden" name="wpEditToken" value="{$token}" />
+</form>
+END
+);
+
+       }
+
+       function doSubmit() {
+               global $wgOut, $wgRequest, $wgReadOnlyFile;
+
+               $wpLockConfirm = $wgRequest->getCheck( 'wpLockConfirm' );
+               if ( ! $wpLockConfirm ) {
+                       $this->showForm( wfMsg( "locknoconfirm" ) );
+                       return;
+               }
+               if ( @! unlink( $wgReadOnlyFile ) ) {
+                       $wgOut->showFileDeleteError( $wgReadOnlyFile );
+                       return;
+               }
+               $titleObj = SpecialPage::getTitleFor( "Unlockdb" );
+               $success = $titleObj->getFullURL( "action=success" );
+               $wgOut->redirect( $success );
+       }
+
+       function showSuccess() {
+               global $wgOut;
+               global $ip;
+
+               $wgOut->setPagetitle( wfMsg( "unlockdb" ) );
+               $wgOut->setSubtitle( wfMsg( "unlockdbsuccesssub" ) );
+               $wgOut->addWikiMsg( "unlockdbsuccesstext", $ip );
+       }
+}
diff --git a/includes/specials/Unusedcategories.php b/includes/specials/Unusedcategories.php
new file mode 100644 (file)
index 0000000..406f794
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class UnusedCategoriesPage extends QueryPage {
+
+       function isExpensive() { return true; }
+
+       function getName() {
+               return 'Unusedcategories';
+       }
+
+       function getPageHeader() {
+               return wfMsgExt( 'unusedcategoriestext', array( 'parse' ) );
+       }
+
+       function getSQL() {
+               $NScat = NS_CATEGORY;
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
+               return "SELECT 'Unusedcategories' as type,
+                               {$NScat} as namespace, page_title as title, page_title as value
+                               FROM $page
+                               LEFT JOIN $categorylinks ON page_title=cl_to
+                               WHERE cl_from IS NULL
+                               AND page_namespace = {$NScat}
+                               AND page_is_redirect = 0";
+       }
+
+       function formatResult( $skin, $result ) {
+               $title = Title::makeTitle( NS_CATEGORY, $result->title );
+               return $skin->makeLinkObj( $title, $title->getText() );
+       }
+}
+
+/** constructor */
+function wfSpecialUnusedCategories() {
+       list( $limit, $offset ) = wfCheckLimits();
+       $uc = new UnusedCategoriesPage();
+       return $uc->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Unusedimages.php b/includes/specials/Unusedimages.php
new file mode 100644 (file)
index 0000000..d71b638
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Unusedimages
+ * @ingroup SpecialPage
+ */
+class UnusedimagesPage extends ImageQueryPage {
+
+       function isExpensive() { return true; }
+
+       function getName() {
+               return 'Unusedimages';
+       }
+
+       function sortDescending() {
+               return false;
+       }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               global $wgCountCategorizedImagesAsUsed;
+               $dbr = wfGetDB( DB_SLAVE );
+
+               if ( $wgCountCategorizedImagesAsUsed ) {
+                       list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' );
+
+                       return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+                                               img_user, img_user_text,  img_description
+                                       FROM ((($page AS I LEFT JOIN $categorylinks AS L ON I.page_id = L.cl_from)
+                                               LEFT JOIN $imagelinks AS P ON I.page_title = P.il_to)
+                                               INNER JOIN $image AS G ON I.page_title = G.img_name)
+                                       WHERE I.page_namespace = ".NS_IMAGE." AND L.cl_from IS NULL AND P.il_to IS NULL";
+               } else {
+                       list( $image, $imagelinks ) = $dbr->tableNamesN( 'image','imagelinks' );
+
+                       return "SELECT 'Unusedimages' as type, 6 as namespace, img_name as title, img_timestamp as value,
+                               img_user, img_user_text,  img_description
+                               FROM $image LEFT JOIN $imagelinks ON img_name=il_to WHERE il_to IS NULL ";
+               }
+       }
+
+       function getPageHeader() {
+               return wfMsgExt( 'unusedimagestext', array( 'parse') );
+       }
+
+}
+
+/**
+ * Entry point
+ */
+function wfSpecialUnusedimages() {
+       list( $limit, $offset ) = wfCheckLimits();
+       $uip = new UnusedimagesPage();
+
+       return $uip->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Unusedtemplates.php b/includes/specials/Unusedtemplates.php
new file mode 100644 (file)
index 0000000..89acd09
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Unusedtemplates
+ * @author Rob Church <robchur@gmail.com>
+ * @copyright Â© 2006 Rob Church
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @ingroup SpecialPage
+ */
+class UnusedtemplatesPage extends QueryPage {
+
+       function getName() { return( 'Unusedtemplates' ); }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+       function sortDescending() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $templatelinks) = $dbr->tableNamesN( 'page', 'templatelinks' );
+               $sql = "SELECT 'Unusedtemplates' AS type, page_title AS title,
+                       page_namespace AS namespace, 0 AS value
+                       FROM $page
+                       LEFT JOIN $templatelinks
+                       ON page_namespace = tl_namespace AND page_title = tl_title
+                       WHERE page_namespace = 10 AND tl_from IS NULL
+                       AND page_is_redirect = 0";
+               return $sql;
+       }
+
+       function formatResult( $skin, $result ) {
+               $title = Title::makeTitle( NS_TEMPLATE, $result->title );
+               $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' );
+               $wlhLink = $skin->makeKnownLinkObj(
+                       SpecialPage::getTitleFor( 'Whatlinkshere' ),
+                       wfMsgHtml( 'unusedtemplateswlh' ),
+                       'target=' . $title->getPrefixedUrl() );
+               return wfSpecialList( $pageLink, $wlhLink );
+       }
+
+       function getPageHeader() {
+               return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) );
+       }
+
+}
+
+function wfSpecialUnusedtemplates() {
+       list( $limit, $offset ) = wfCheckLimits();
+       $utp = new UnusedtemplatesPage();
+       $utp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Unwatchedpages.php b/includes/specials/Unwatchedpages.php
new file mode 100644 (file)
index 0000000..64ab372
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A special page that displays a list of pages that are not on anyones watchlist.
+ * Implements Special:Unwatchedpages
+ *
+ * @ingroup SpecialPage
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class UnwatchedpagesPage extends QueryPage {
+
+       function getName() { return 'Unwatchedpages'; }
+       function isExpensive() { return true; }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $watchlist ) = $dbr->tableNamesN( 'page', 'watchlist' );
+               $mwns = NS_MEDIAWIKI;
+               return
+                       "
+                       SELECT
+                               'Unwatchedpages' as type,
+                               page_namespace as namespace,
+                               page_title as title,
+                               page_namespace as value
+                       FROM $page
+                       LEFT JOIN $watchlist ON wl_namespace = page_namespace AND page_title = wl_title
+                       WHERE wl_title IS NULL AND page_is_redirect = 0 AND page_namespace<>$mwns
+                       ";
+       }
+
+       function sortDescending() { return false; }
+
+       function formatResult( $skin, $result ) {
+               global $wgContLang;
+
+               $nt = Title::makeTitle( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getPrefixedText() );
+
+               $plink = $skin->makeKnownLinkObj( $nt, htmlspecialchars( $text ) );
+               $wlink = $skin->makeKnownLinkObj( $nt, wfMsgHtml( 'watch' ), 'action=watch' );
+
+               return wfSpecialList( $plink, $wlink );
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialUnwatchedpages() {
+       global $wgUser, $wgOut;
+
+       if ( ! $wgUser->isAllowed( 'unwatchedpages' ) )
+               return $wgOut->permissionRequired( 'unwatchedpages' );
+
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new UnwatchedpagesPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Upload.php b/includes/specials/Upload.php
new file mode 100644 (file)
index 0000000..0f37f50
--- /dev/null
@@ -0,0 +1,1755 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+
+/**
+ * Entry point
+ */
+function wfSpecialUpload() {
+       global $wgRequest;
+       $form = new UploadForm( $wgRequest );
+       $form->execute();
+}
+
+/**
+ * implements Special:Upload
+ * @ingroup SpecialPage
+ */
+class UploadForm {
+       const SUCCESS = 0;
+       const BEFORE_PROCESSING = 1;
+       const LARGE_FILE_SERVER = 2;
+       const EMPTY_FILE = 3;
+       const MIN_LENGHT_PARTNAME = 4;
+       const ILLEGAL_FILENAME = 5;
+       const PROTECTED_PAGE = 6;
+       const OVERWRITE_EXISTING_FILE = 7;
+       const FILETYPE_MISSING = 8;
+       const FILETYPE_BADTYPE = 9;
+       const VERIFICATION_ERROR = 10;
+       const UPLOAD_VERIFICATION_ERROR = 11;
+       const UPLOAD_WARNING = 12;
+       const INTERNAL_ERROR = 13;
+
+       /**#@+
+        * @access private
+        */
+       var $mComment, $mLicense, $mIgnoreWarning, $mCurlError;
+       var $mDestName, $mTempPath, $mFileSize, $mFileProps;
+       var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked;
+       var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType;
+       var $mDestWarningAck, $mCurlDestHandle;
+       var $mLocalFile;
+
+       # Placeholders for text injection by hooks (must be HTML)
+       # extensions should take care to _append_ to the present value
+       var $uploadFormTextTop;
+       var $uploadFormTextAfterSummary;
+
+       const SESSION_VERSION = 1;
+       /**#@-*/
+
+       /**
+        * Constructor : initialise object
+        * Get data POSTed through the form and assign them to the object
+        * @param $request Data posted.
+        */
+       function UploadForm( &$request ) {
+               global $wgAllowCopyUploads;
+               $this->mDesiredDestName   = $request->getText( 'wpDestFile' );
+               $this->mIgnoreWarning     = $request->getCheck( 'wpIgnoreWarning' );
+               $this->mComment           = $request->getText( 'wpUploadDescription' );
+
+               if( !$request->wasPosted() ) {
+                       # GET requests just give the main form; no data except destination
+                       # filename and description
+                       return;
+               }
+
+               # Placeholders for text injection by hooks (empty per default)
+               $this->uploadFormTextTop = "";
+               $this->uploadFormTextAfterSummary = "";
+
+               $this->mReUpload          = $request->getCheck( 'wpReUpload' );
+               $this->mUploadClicked     = $request->getCheck( 'wpUpload' );
+
+               $this->mLicense           = $request->getText( 'wpLicense' );
+               $this->mCopyrightStatus   = $request->getText( 'wpUploadCopyStatus' );
+               $this->mCopyrightSource   = $request->getText( 'wpUploadSource' );
+               $this->mWatchthis         = $request->getBool( 'wpWatchthis' );
+               $this->mSourceType        = $request->getText( 'wpSourceType' );
+               $this->mDestWarningAck    = $request->getText( 'wpDestFileWarningAck' );
+
+               $this->mAction            = $request->getVal( 'action' );
+
+               $this->mSessionKey        = $request->getInt( 'wpSessionKey' );
+               if( !empty( $this->mSessionKey ) &&
+                       isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) &&
+                       $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) {
+                       /**
+                        * Confirming a temporarily stashed upload.
+                        * We don't want path names to be forged, so we keep
+                        * them in the session on the server and just give
+                        * an opaque key to the user agent.
+                        */
+                       $data = $_SESSION['wsUploadData'][$this->mSessionKey];
+                       $this->mTempPath         = $data['mTempPath'];
+                       $this->mFileSize         = $data['mFileSize'];
+                       $this->mSrcName          = $data['mSrcName'];
+                       $this->mFileProps        = $data['mFileProps'];
+                       $this->mCurlError        = 0/*UPLOAD_ERR_OK*/;
+                       $this->mStashed          = true;
+                       $this->mRemoveTempFile   = false;
+               } else {
+                       /**
+                        *Check for a newly uploaded file.
+                        */
+                       if( $wgAllowCopyUploads && $this->mSourceType == 'web' ) {
+                               $this->initializeFromUrl( $request );
+                       } else {
+                               $this->initializeFromUpload( $request );
+                       }
+               }
+       }
+
+       /**
+        * Initialize the uploaded file from PHP data
+        * @access private
+        */
+       function initializeFromUpload( $request ) {
+               $this->mTempPath       = $request->getFileTempName( 'wpUploadFile' );
+               $this->mFileSize       = $request->getFileSize( 'wpUploadFile' );
+               $this->mSrcName        = $request->getFileName( 'wpUploadFile' );
+               $this->mCurlError      = $request->getUploadError( 'wpUploadFile' );
+               $this->mSessionKey     = false;
+               $this->mStashed        = false;
+               $this->mRemoveTempFile = false; // PHP will handle this
+       }
+
+       /**
+        * Copy a web file to a temporary file
+        * @access private
+        */
+       function initializeFromUrl( $request ) {
+               global $wgTmpDirectory;
+               $url = $request->getText( 'wpUploadFileURL' );
+               $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
+
+               $this->mTempPath       = $local_file;
+               $this->mFileSize       = 0; # Will be set by curlCopy
+               $this->mCurlError      = $this->curlCopy( $url, $local_file );
+               $pathParts             = explode( '/', $url );
+               $this->mSrcName        = array_pop( $pathParts );
+               $this->mSessionKey     = false;
+               $this->mStashed        = false;
+
+               // PHP won't auto-cleanup the file
+               $this->mRemoveTempFile = file_exists( $local_file );
+       }
+
+       /**
+        * Safe copy from URL
+        * Returns true if there was an error, false otherwise
+        */
+       private function curlCopy( $url, $dest ) {
+               global $wgUser, $wgOut;
+
+               if( !$wgUser->isAllowed( 'upload_by_url' ) ) {
+                       $wgOut->permissionRequired( 'upload_by_url' );
+                       return true;
+               }
+
+               # Maybe remove some pasting blanks :-)
+               $url =  trim( $url );
+               if( stripos($url, 'http://') !== 0 && stripos($url, 'ftp://') !== 0 ) {
+                       # Only HTTP or FTP URLs
+                       $wgOut->showErrorPage( 'upload-proto-error', 'upload-proto-error-text' );
+                       return true;
+               }
+
+               # Open temporary file
+               $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
+               if( $this->mCurlDestHandle === false ) {
+                       # Could not open temporary file to write in
+                       $wgOut->showErrorPage( 'upload-file-error', 'upload-file-error-text');
+                       return true;
+               }
+
+               $ch = curl_init();
+               curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug
+               curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout
+               curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed
+               curl_setopt( $ch, CURLOPT_URL, $url);
+               curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) );
+               curl_exec( $ch );
+               $error = curl_errno( $ch ) ? true : false;
+               $errornum =  curl_errno( $ch );
+               // if ( $error ) print curl_error ( $ch ) ; # Debugging output
+               curl_close( $ch );
+
+               fclose( $this->mCurlDestHandle );
+               unset( $this->mCurlDestHandle );
+               if( $error ) {
+                       unlink( $dest );
+                       if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) )
+                               $wgOut->showErrorPage( 'upload-misc-error', 'upload-misc-error-text' );
+                       else
+                               $wgOut->showErrorPage( "upload-curl-error$errornum", "upload-curl-error$errornum-text" );
+               }
+
+               return $error;
+       }
+
+       /**
+        * Callback function for CURL-based web transfer
+        * Write data to file unless we've passed the length limit;
+        * if so, abort immediately.
+        * @access private
+        */
+       function uploadCurlCallback( $ch, $data ) {
+               global $wgMaxUploadSize;
+               $length = strlen( $data );
+               $this->mFileSize += $length;
+               if( $this->mFileSize > $wgMaxUploadSize ) {
+                       return 0;
+               }
+               fwrite( $this->mCurlDestHandle, $data );
+               return $length;
+       }
+
+       /**
+        * Start doing stuff
+        * @access public
+        */
+       function execute() {
+               global $wgUser, $wgOut;
+               global $wgEnableUploads;
+
+               # Check uploading enabled
+               if( !$wgEnableUploads ) {
+                       $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
+                       return;
+               }
+
+               # Check permissions
+               if( !$wgUser->isAllowed( 'upload' ) ) {
+                       if( !$wgUser->isLoggedIn() ) {
+                               $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+                       } else {
+                               $wgOut->permissionRequired( 'upload' );
+                       }
+                       return;
+               }
+
+               # Check blocks
+               if( $wgUser->isBlocked() ) {
+                       $wgOut->blockedPage();
+                       return;
+               }
+
+               if( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+
+               if( $this->mReUpload ) {
+                       if( !$this->unsaveUploadedFile() ) {
+                               return;
+                       }
+                       # Because it is probably checked and shouldn't be
+                       $this->mIgnoreWarning = false;
+                       
+                       $this->mainUploadForm();
+               } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
+                       $this->processUpload();
+               } else {
+                       $this->mainUploadForm();
+               }
+
+               $this->cleanupTempFile();
+       }
+
+       /**
+        * Do the upload
+        * Checks are made in SpecialUpload::execute()
+        *
+        * @access private
+        */
+       function processUpload(){
+               global $wgUser, $wgOut, $wgFileExtensions;
+               $details = null;
+               $value = null;
+               $value = $this->internalProcessUpload( $details );
+
+               switch($value) {
+                       case self::SUCCESS:
+                               $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
+                               break;
+
+                       case self::BEFORE_PROCESSING:
+                               break;
+
+                       case self::LARGE_FILE_SERVER:
+                               $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) );
+                               break;
+
+                       case self::EMPTY_FILE:
+                               $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) );
+                               break;
+
+                       case self::MIN_LENGHT_PARTNAME:
+                               $this->mainUploadForm( wfMsgHtml( 'minlength1' ) );
+                               break;
+
+                       case self::ILLEGAL_FILENAME:
+                               $filtered = $details['filtered'];
+                               $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) );
+                               break;
+
+                       case self::PROTECTED_PAGE:
+                               $wgOut->showPermissionsErrorPage( $details['permissionserrors'] );
+                               break;
+
+                       case self::OVERWRITE_EXISTING_FILE:
+                               $errorText = $details['overwrite'];
+                               $overwrite = new WikiError( $wgOut->parse( $errorText ) );
+                               $this->uploadError( $overwrite->toString() );
+                               break;
+
+                       case self::FILETYPE_MISSING:
+                               $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) );
+                               break;
+
+                       case self::FILETYPE_BADTYPE:
+                               $finalExt = $details['finalExt'];
+                               $this->uploadError(
+                                       wfMsgExt( 'filetype-banned-type',
+                                               array( 'parseinline' ),
+                                               htmlspecialchars( $finalExt ),
+                                               implode(
+                                                       wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+                                                       $wgFileExtensions
+                                               )
+                                       )
+                               );
+                               break;
+
+                       case self::VERIFICATION_ERROR:
+                               $veri = $details['veri'];
+                               $this->uploadError( $veri->toString() );
+                               break;
+
+                       case self::UPLOAD_VERIFICATION_ERROR:
+                               $error = $details['error'];
+                               $this->uploadError( $error );
+                               break;
+
+                       case self::UPLOAD_WARNING:
+                               $warning = $details['warning'];
+                               $this->uploadWarning( $warning );
+                               break;
+
+                       case self::INTERNAL_ERROR:
+                               $internal = $details['internal'];
+                               $this->showError( $internal );
+                               break;
+
+                       default:
+                               throw new MWException( __METHOD__ . ": Unknown value `{$value}`" );
+               }
+       }
+
+       /**
+        * Really do the upload
+        * Checks are made in SpecialUpload::execute()
+        *
+        * @param array $resultDetails contains result-specific dict of additional values
+        *
+        * @access private
+        */
+       function internalProcessUpload( &$resultDetails ) {
+               global $wgUser;
+
+               if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
+               {
+                       wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file." );
+                       return self::BEFORE_PROCESSING;
+               }
+
+               /**
+                * If there was no filename or a zero size given, give up quick.
+                */
+               if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
+                       return self::EMPTY_FILE;
+               }
+
+               /* Check for curl error */
+               if( $this->mCurlError ) {
+                       return self::BEFORE_PROCESSING;
+               }
+
+               # Chop off any directories in the given filename
+               if( $this->mDesiredDestName ) {
+                       $basename = $this->mDesiredDestName;
+               } else {
+                       $basename = $this->mSrcName;
+               }
+               $filtered = wfBaseName( $basename );
+
+               /**
+                * We'll want to blacklist against *any* 'extension', and use
+                * only the final one for the whitelist.
+                */
+               list( $partname, $ext ) = $this->splitExtensions( $filtered );
+
+               if( count( $ext ) ) {
+                       $finalExt = $ext[count( $ext ) - 1];
+               } else {
+                       $finalExt = '';
+               }
+
+               # If there was more than one "extension", reassemble the base
+               # filename to prevent bogus complaints about length
+               if( count( $ext ) > 1 ) {
+                       for( $i = 0; $i < count( $ext ) - 1; $i++ )
+                               $partname .= '.' . $ext[$i];
+               }
+
+               if( strlen( $partname ) < 1 ) {
+                       return self::MIN_LENGHT_PARTNAME;
+               }
+
+               /**
+                * Filter out illegal characters, and try to make a legible name
+                * out of it. We'll strip some silently that Title would die on.
+                */
+               $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered );
+               $nt = Title::makeTitleSafe( NS_IMAGE, $filtered );
+               if( is_null( $nt ) ) {
+                       $resultDetails = array( 'filtered' => $filtered );
+                       return self::ILLEGAL_FILENAME;
+               }
+               $this->mLocalFile = wfLocalFile( $nt );
+               $this->mDestName = $this->mLocalFile->getName();
+
+               /**
+                * If the image is protected, non-sysop users won't be able
+                * to modify it by uploading a new revision.
+                */
+               $permErrors = $nt->getUserPermissionsErrors( 'edit', $wgUser );
+               $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $wgUser );
+               $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $wgUser ) );
+
+               if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
+                       // merge all the problems into one list, avoiding duplicates
+                       $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
+                       $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
+                       $resultDetails = array( 'permissionserrors' => $permErrors );
+                       return self::PROTECTED_PAGE;
+               }
+
+               /**
+                * In some cases we may forbid overwriting of existing files.
+                */
+               $overwrite = $this->checkOverwrite( $this->mDestName );
+               if( $overwrite !== true ) {
+                       $resultDetails = array( 'overwrite' => $overwrite );
+                       return self::OVERWRITE_EXISTING_FILE;
+               }
+
+               /* Don't allow users to override the blacklist (check file extension) */
+               global $wgCheckFileExtensions, $wgStrictFileExtensions;
+               global $wgFileExtensions, $wgFileBlacklist;
+               if ($finalExt == '') {
+                       return self::FILETYPE_MISSING;
+               } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
+                               ($wgCheckFileExtensions && $wgStrictFileExtensions &&
+                                       !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) {
+                       $resultDetails = array( 'finalExt' => $finalExt );
+                       return self::FILETYPE_BADTYPE;
+               }
+
+               /**
+                * Look at the contents of the file; if we can recognize the
+                * type but it's corrupt or data of the wrong type, we should
+                * probably not accept it.
+                */
+               if( !$this->mStashed ) {
+                       $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt );
+                       $this->checkMacBinary();
+                       $veri = $this->verify( $this->mTempPath, $finalExt );
+
+                       if( $veri !== true ) { //it's a wiki error...
+                               $resultDetails = array( 'veri' => $veri );
+                               return self::VERIFICATION_ERROR;
+                       }
+
+                       /**
+                        * Provide an opportunity for extensions to add further checks
+                        */
+                       $error = '';
+                       if( !wfRunHooks( 'UploadVerification',
+                                       array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+                               $resultDetails = array( 'error' => $error );
+                               return self::UPLOAD_VERIFICATION_ERROR;
+                       }
+               }
+
+
+               /**
+                * Check for non-fatal conditions
+                */
+               if ( ! $this->mIgnoreWarning ) {
+                       $warning = '';
+
+                       global $wgCapitalLinks;
+                       if( $wgCapitalLinks ) {
+                               $filtered = ucfirst( $filtered );
+                       }
+                       if( $basename != $filtered ) {
+                               $warning .=  '<li>'.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'</li>';
+                       }
+
+                       global $wgCheckFileExtensions;
+                       if ( $wgCheckFileExtensions ) {
+                               if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) {
+                                       $warning .= '<li>' .
+                                       wfMsgExt( 'filetype-unwanted-type',
+                                               array( 'parseinline' ),
+                                               htmlspecialchars( $finalExt ),
+                                               implode(
+                                                       wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+                                                       $wgFileExtensions
+                                               )
+                                       ) . '</li>';
+                               }
+                       }
+
+                       global $wgUploadSizeWarning;
+                       if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
+                               $skin = $wgUser->getSkin();
+                               $wsize = $skin->formatSize( $wgUploadSizeWarning );
+                               $asize = $skin->formatSize( $this->mFileSize );
+                               $warning .= '<li>' . wfMsgHtml( 'large-file', $wsize, $asize ) . '</li>';
+                       }
+                       if ( $this->mFileSize == 0 ) {
+                               $warning .= '<li>'.wfMsgHtml( 'emptyfile' ).'</li>';
+                       }
+
+                       if ( !$this->mDestWarningAck ) {
+                               $warning .= self::getExistsWarning( $this->mLocalFile );
+                       }
+                       
+                       $warning .= $this->getDupeWarning( $this->mTempPath );
+                       
+                       if( $warning != '' ) {
+                               /**
+                                * Stash the file in a temporary location; the user can choose
+                                * to let it through and we'll complete the upload then.
+                                */
+                               $resultDetails = array( 'warning' => $warning );
+                               return self::UPLOAD_WARNING;
+                       }
+               }
+
+               /**
+                * Try actually saving the thing...
+                * It will show an error form on failure.
+                */
+               $pageText = self::getInitialPageText( $this->mComment, $this->mLicense,
+                       $this->mCopyrightStatus, $this->mCopyrightSource );
+
+               $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText,
+                       File::DELETE_SOURCE, $this->mFileProps );
+               if ( !$status->isGood() ) {
+                       $resultDetails = array( 'internal' => $status->getWikiText() );
+                       return self::INTERNAL_ERROR;
+               } else {
+                       if ( $this->mWatchthis ) {
+                               global $wgUser;
+                               $wgUser->addWatch( $this->mLocalFile->getTitle() );
+                       }
+                       // Success, redirect to description page
+                       $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere?
+                       wfRunHooks( 'UploadComplete', array( &$this ) );
+                       return self::SUCCESS;
+               }
+       }
+
+       /**
+        * Do existence checks on a file and produce a warning
+        * This check is static and can be done pre-upload via AJAX
+        * Returns an HTML fragment consisting of one or more LI elements if there is a warning
+        * Returns an empty string if there is no warning
+        */
+       static function getExistsWarning( $file ) {
+               global $wgUser, $wgContLang;
+               // Check for uppercase extension. We allow these filenames but check if an image
+               // with lowercase extension exists already
+               $warning = '';
+               $align = $wgContLang->isRtl() ? 'left' : 'right';
+
+               if( strpos( $file->getName(), '.' ) == false ) {
+                       $partname = $file->getName();
+                       $rawExtension = '';
+               } else {
+                       $n = strrpos( $file->getName(), '.' );
+                       $rawExtension = substr( $file->getName(), $n + 1 );
+                       $partname = substr( $file->getName(), 0, $n );
+               }
+
+               $sk = $wgUser->getSkin();
+
+               if ( $rawExtension != $file->getExtension() ) {
+                       // We're not using the normalized form of the extension.
+                       // Normal form is lowercase, using most common of alternate
+                       // extensions (eg 'jpg' rather than 'JPEG').
+                       //
+                       // Check for another file using the normalized form...
+                       $nt_lc = Title::makeTitle( NS_IMAGE, $partname . '.' . $file->getExtension() );
+                       $file_lc = wfLocalFile( $nt_lc );
+               } else {
+                       $file_lc = false;
+               }
+
+               if( $file->exists() ) {
+                       $dlink = $sk->makeKnownLinkObj( $file->getTitle() );
+                       if ( $file->allowInlineDisplay() ) {
+                               $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+                                       $file->getName(), $align, array(), false, true );
+                       } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) {
+                               $icon = $file->iconThumb();
+                               $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
+                                       $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
+                       } else {
+                               $dlink2 = '';
+                       }
+
+                       $warning .= '<li>' . wfMsgExt( 'fileexists', array('parseinline','replaceafter'), $dlink ) . '</li>' . $dlink2;
+
+               } elseif( $file->getTitle()->getArticleID() ) {
+                       $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' );
+                       $warning .= '<li>' . wfMsgExt( 'filepageexists', array( 'parseinline', 'replaceafter' ), $lnk ) . '</li>';
+               } elseif ( $file_lc && $file_lc->exists() ) {
+                       # Check if image with lowercase extension exists.
+                       # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension
+                       $dlink = $sk->makeKnownLinkObj( $nt_lc );
+                       if ( $file_lc->allowInlineDisplay() ) {
+                               $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+                                       $nt_lc->getText(), $align, array(), false, true );
+                       } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) {
+                               $icon = $file_lc->iconThumb();
+                               $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
+                                       $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' . $dlink . '</div>';
+                       } else {
+                               $dlink2 = '';
+                       }
+
+                       $warning .= '<li>' .
+                               wfMsgExt( 'fileexists-extension', 'parsemag',
+                                       $file->getTitle()->getPrefixedText(), $dlink ) .
+                               '</li>' . $dlink2;
+
+               } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' )
+                       && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) )
+               {
+                       # Check for filenames like 50px- or 180px-, these are mostly thumbnails
+                       $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension );
+                       $file_thb = wfLocalFile( $nt_thb );
+                       if ($file_thb->exists() ) {
+                               # Check if an image without leading '180px-' (or similiar) exists
+                               $dlink = $sk->makeKnownLinkObj( $nt_thb);
+                               if ( $file_thb->allowInlineDisplay() ) {
+                                       $dlink2 = $sk->makeImageLinkObj( $nt_thb,
+                                               wfMsgExt( 'fileexists-thumb', 'parseinline' ),
+                                               $nt_thb->getText(), $align, array(), false, true );
+                               } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) {
+                                       $icon = $file_thb->iconThumb();
+                                       $dlink2 = '<div style="float:' . $align . '" id="mw-media-icon">' .
+                                               $icon->toHtml( array( 'desc-link' => true ) ) . '<br />' .
+                                               $dlink . '</div>';
+                               } else {
+                                       $dlink2 = '';
+                               }
+
+                               $warning .= '<li>' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) .
+                                       '</li>' . $dlink2;
+                       } else {
+                               # Image w/o '180px-' does not exists, but we do not like these filenames
+                               $warning .= '<li>' . wfMsgExt( 'file-thumbnail-no', 'parseinline' ,
+                                       substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '</li>';
+                       }
+               }
+
+               $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist();
+               # Do the match
+               foreach( $filenamePrefixBlacklist as $prefix ) {
+                       if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
+                               $warning .= '<li>' . wfMsgExt( 'filename-bad-prefix', 'parseinline', $prefix ) . '</li>';
+                               break;
+                       }
+               }
+
+               if ( $file->wasDeleted() && !$file->exists() ) {
+                       # If the file existed before and was deleted, warn the user of this
+                       # Don't bother doing so if the file exists now, however
+                       $ltitle = SpecialPage::getTitleFor( 'Log' );
+                       $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ),
+                               'type=delete&page=' . $file->getTitle()->getPrefixedUrl() );
+                       $warning .= '<li>' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '</li>';
+               }
+               return $warning;
+       }
+
+       /**
+        * Get a list of warnings
+        *
+        * @param string local filename, e.g. 'file exists', 'non-descriptive filename'
+        * @return array list of warning messages
+        */
+       static function ajaxGetExistsWarning( $filename ) {
+               $file = wfFindFile( $filename );
+               if( !$file ) {
+                       // Force local file so we have an object to do further checks against
+                       // if there isn't an exact match...
+                       $file = wfLocalFile( $filename );
+               }
+               $s = '&nbsp;';
+               if ( $file ) {
+                       $warning = self::getExistsWarning( $file );
+                       if ( $warning !== '' ) {
+                               $s = "<ul>$warning</ul>";
+                       }
+               }
+               return $s;
+       }
+
+       /**
+        * Render a preview of a given license for the AJAX preview on upload
+        *
+        * @param string $license
+        * @return string
+        */
+       public static function ajaxGetLicensePreview( $license ) {
+               global $wgParser, $wgUser;
+               $text = '{{' . $license . '}}';
+               $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' );
+               $options = ParserOptions::newFromUser( $wgUser );
+
+               // Expand subst: first, then live templates...
+               $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options );
+               $output = $wgParser->parse( $text, $title, $options );
+
+               return $output->getText();
+       }
+       
+       /**
+        * Check for duplicate files and throw up a warning before the upload
+        * completes.
+        */
+       function getDupeWarning( $tempfile ) {
+               $hash = File::sha1Base36( $tempfile );
+               $dupes = RepoGroup::singleton()->findBySha1( $hash );
+               if( $dupes ) {
+                       global $wgOut;
+                       $msg = "<gallery>";
+                       foreach( $dupes as $file ) {
+                               $title = $file->getTitle();
+                               $msg .= $title->getPrefixedText() .
+                                       "|" . $title->getText() . "\n";
+                       }
+                       $msg .= "</gallery>";
+                       return "<li>" .
+                               wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) .
+                               $wgOut->parse( $msg ) .
+                               "</li>\n";
+               } else {
+                       return '';
+               }
+       }
+
+       /**
+        * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]]
+        *
+        * @return array list of prefixes
+        */
+       public static function getFilenamePrefixBlacklist() {
+               $blacklist = array();
+               $message = wfMsgForContent( 'filename-prefix-blacklist' );
+               if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
+                       $lines = explode( "\n", $message );
+                       foreach( $lines as $line ) {
+                               // Remove comment lines
+                               $comment = substr( trim( $line ), 0, 1 );
+                               if ( $comment == '#' || $comment == '' ) {
+                                       continue;
+                               }
+                               // Remove additional comments after a prefix
+                               $comment = strpos( $line, '#' );
+                               if ( $comment > 0 ) {
+                                       $line = substr( $line, 0, $comment-1 );
+                               }
+                               $blacklist[] = trim( $line );
+                       }
+               }
+               return $blacklist;
+       }
+
+       /**
+        * Stash a file in a temporary directory for later processing
+        * after the user has confirmed it.
+        *
+        * If the user doesn't explicitly cancel or accept, these files
+        * can accumulate in the temp directory.
+        *
+        * @param string $saveName - the destination filename
+        * @param string $tempName - the source temporary file to save
+        * @return string - full path the stashed file, or false on failure
+        * @access private
+        */
+       function saveTempUploadedFile( $saveName, $tempName ) {
+               global $wgOut;
+               $repo = RepoGroup::singleton()->getLocalRepo();
+               $status = $repo->storeTemp( $saveName, $tempName );
+               if ( !$status->isGood() ) {
+                       $this->showError( $status->getWikiText() );
+                       return false;
+               } else {
+                       return $status->value;
+               }
+       }
+
+       /**
+        * Stash a file in a temporary directory for later processing,
+        * and save the necessary descriptive info into the session.
+        * Returns a key value which will be passed through a form
+        * to pick up the path info on a later invocation.
+        *
+        * @return int
+        * @access private
+        */
+       function stashSession() {
+               $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
+
+               if( !$stash ) {
+                       # Couldn't save the file.
+                       return false;
+               }
+
+               $key = mt_rand( 0, 0x7fffffff );
+               $_SESSION['wsUploadData'][$key] = array(
+                       'mTempPath'       => $stash,
+                       'mFileSize'       => $this->mFileSize,
+                       'mSrcName'        => $this->mSrcName,
+                       'mFileProps'      => $this->mFileProps,
+                       'version'         => self::SESSION_VERSION,
+               );
+               return $key;
+       }
+
+       /**
+        * Remove a temporarily kept file stashed by saveTempUploadedFile().
+        * @access private
+        * @return success
+        */
+       function unsaveUploadedFile() {
+               global $wgOut;
+               $repo = RepoGroup::singleton()->getLocalRepo();
+               $success = $repo->freeTemp( $this->mTempPath );
+               if ( ! $success ) {
+                       $wgOut->showFileDeleteError( $this->mTempPath );
+                       return false;
+               } else {
+                       return true;
+               }
+       }
+
+       /* -------------------------------------------------------------- */
+
+       /**
+        * @param string $error as HTML
+        * @access private
+        */
+       function uploadError( $error ) {
+               global $wgOut;
+               $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+               $wgOut->addHTML( '<span class="error">' . $error . '</span>' );
+       }
+
+       /**
+        * There's something wrong with this file, not enough to reject it
+        * totally but we require manual intervention to save it for real.
+        * Stash it away, then present a form asking to confirm or cancel.
+        *
+        * @param string $warning as HTML
+        * @access private
+        */
+       function uploadWarning( $warning ) {
+               global $wgOut;
+               global $wgUseCopyrightUpload;
+
+               $this->mSessionKey = $this->stashSession();
+               if( !$this->mSessionKey ) {
+                       # Couldn't save file; an error has been displayed so let's go.
+                       return;
+               }
+
+               $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
+               $wgOut->addHTML( '<ul class="warning">' . $warning . "</ul>\n" );
+
+               $titleObj = SpecialPage::getTitleFor( 'Upload' );
+
+               if ( $wgUseCopyrightUpload ) {
+                       $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" .
+                                       Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n";
+               } else {
+                       $copyright = '';
+               }
+
+               $wgOut->addHTML(
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ),
+                                'enctype' => 'multipart/form-data', 'id' => 'uploadwarning' ) ) . "\n" .
+                       Xml::hidden( 'wpIgnoreWarning', '1' ) . "\n" .
+                       Xml::hidden( 'wpSessionKey', $this->mSessionKey ) . "\n" .
+                       Xml::hidden( 'wpUploadDescription', $this->mComment ) . "\n" .
+                       Xml::hidden( 'wpLicense', $this->mLicense ) . "\n" .
+                       Xml::hidden( 'wpDestFile', $this->mDesiredDestName ) . "\n" .
+                       Xml::hidden( 'wpWatchthis', $this->mWatchthis ) . "\n" .
+                       "{$copyright}<br />" .
+                       Xml::submitButton( wfMsg( 'ignorewarning' ), array ( 'name' => 'wpUpload', 'id' => 'wpUpload', 'checked' => 'checked' ) ) . ' ' .
+                       Xml::submitButton( wfMsg( 'reuploaddesc' ), array ( 'name' => 'wpReUpload', 'id' => 'wpReUpload' ) ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+       }
+
+       /**
+        * Displays the main upload form, optionally with a highlighted
+        * error message up at the top.
+        *
+        * @param string $msg as HTML
+        * @access private
+        */
+       function mainUploadForm( $msg='' ) {
+               global $wgOut, $wgUser, $wgLang, $wgMaxUploadSize;
+               global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview;
+               global $wgRequest, $wgAllowCopyUploads;
+               global $wgStylePath, $wgStyleVersion;
+
+               $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
+               $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview;
+
+               $adc = wfBoolToStr( $useAjaxDestCheck );
+               $alp = wfBoolToStr( $useAjaxLicensePreview );
+               $autofill = wfBoolToStr( $this->mDesiredDestName == '' );
+
+               $wgOut->addScript( "<script type=\"text/javascript\">
+wgAjaxUploadDestCheck = {$adc};
+wgAjaxLicensePreview = {$alp};
+wgUploadAutoFill = {$autofill};
+</script>" );
+               $wgOut->addScriptFile( 'upload.js' );
+               $wgOut->addScriptFile( 'edit.js' ); // For <charinsert> support
+
+               if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) )
+               {
+                       wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
+                       return false;
+               }
+
+               if( $this->mDesiredDestName ) {
+                       $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName );
+                       // Show a subtitle link to deleted revisions (to sysops et al only)
+                       if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) {
+                               $link = wfMsgExt(
+                                       $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
+                                       array( 'parse', 'replaceafter' ),
+                                       $wgUser->getSkin()->makeKnownLinkObj(
+                                               SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
+                                               wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
+                                       )
+                               );
+                               $wgOut->addHtml( "<div id=\"contentSub2\">{$link}</div>" );
+                       }
+
+                       // Show the relevant lines from deletion log (for still deleted files only)
+                       if( $title instanceof Title && $title->isDeleted() > 0 && !$title->exists() ) {
+                               $this->showDeletionLog( $wgOut, $title->getPrefixedText() );
+                       }
+               }
+
+               $cols = intval($wgUser->getOption( 'cols' ));
+
+               if( $wgUser->getOption( 'editwidth' ) ) {
+                       $width = " style=\"width:100%\"";
+               } else {
+                       $width = '';
+               }
+
+               if ( '' != $msg ) {
+                       $sub = wfMsgHtml( 'uploaderror' );
+                       $wgOut->addHTML( "<h2>{$sub}</h2>\n" .
+                         "<span class='error'>{$msg}</span>\n" );
+               }
+               $wgOut->addHTML( '<div id="uploadtext">' );
+               $wgOut->addWikiMsg( 'uploadtext', $this->mDesiredDestName );
+               $wgOut->addHTML( "</div>\n" );
+
+               # Print a list of allowed file extensions, if so configured.  We ignore
+               # MIME type here, it's incomprehensible to most people and too long.
+               global $wgCheckFileExtensions, $wgStrictFileExtensions,
+               $wgFileExtensions, $wgFileBlacklist;
+
+               $allowedExtensions = '';
+               if( $wgCheckFileExtensions ) {
+                       $delim = wfMsgExt( 'comma-separator', array( 'escapenoentities' ) );
+                       if( $wgStrictFileExtensions ) {
+                               # Everything not permitted is banned
+                               $extensionsList =
+                                       '<div id="mw-upload-permitted">' .
+                                       wfMsgWikiHtml( 'upload-permitted', implode( $wgFileExtensions, $delim ) ) .
+                                       "</div>\n";
+                       } else {
+                               # We have to list both preferred and prohibited
+                               $extensionsList =
+                                       '<div id="mw-upload-preferred">' .
+                                       wfMsgWikiHtml( 'upload-preferred', implode( $wgFileExtensions, $delim ) ) .
+                                       "</div>\n" .
+                                       '<div id="mw-upload-prohibited">' .
+                                       wfMsgWikiHtml( 'upload-prohibited', implode( $wgFileBlacklist, $delim ) ) .
+                                       "</div>\n";
+                       }
+               } else {
+                       # Everything is permitted.
+                       $extensionsList = '';
+               }
+
+               # Get the maximum file size from php.ini as $wgMaxUploadSize works for uploads from URL via CURL only
+               # See http://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize for possible values of upload_max_filesize
+               $val = trim( ini_get( 'upload_max_filesize' ) );
+               $last = strtoupper( ( substr( $val, -1 ) ) );
+               switch( $last ) {
+                       case 'G':
+                               $val2 = substr( $val, 0, -1 ) * 1024 * 1024 * 1024;
+                               break;
+                       case 'M':
+                               $val2 = substr( $val, 0, -1 ) * 1024 * 1024;
+                               break;
+                       case 'K':
+                               $val2 = substr( $val, 0, -1 ) * 1024;
+                               break;
+                       default:
+                               $val2 = $val;
+               }
+               $val2 = $wgAllowCopyUploads ? min( $wgMaxUploadSize, $val2 ) : $val2;
+               $maxUploadSize = '<div id="mw-upload-maxfilesize">' . 
+                       wfMsgExt( 'upload-maxfilesize', array( 'parseinline', 'escapenoentities' ), 
+                               $wgLang->formatSize( $val2 ) ) .
+                               "</div>\n";
+
+               $sourcefilename = wfMsgExt( 'sourcefilename', array( 'parseinline', 'escapenoentities' ) );
+        $destfilename = wfMsgExt( 'destfilename', array( 'parseinline', 'escapenoentities' ) ); 
+               
+               $summary = wfMsgExt( 'fileuploadsummary', 'parseinline' );
+
+               $licenses = new Licenses();
+               $license = wfMsgExt( 'license', array( 'parseinline' ) );
+               $nolicense = wfMsgHtml( 'nolicense' );
+               $licenseshtml = $licenses->getHtml();
+
+               $ulb = wfMsgHtml( 'uploadbtn' );
+
+
+               $titleObj = SpecialPage::getTitleFor( 'Upload' );
+
+               $encDestName = htmlspecialchars( $this->mDesiredDestName );
+
+               $watchChecked = $this->watchCheck()
+                       ? 'checked="checked"'
+                       : '';
+               $warningChecked = $this->mIgnoreWarning ? 'checked' : '';
+
+               // Prepare form for upload or upload/copy
+               if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) {
+                       $filename_form =
+                               "<input type='radio' id='wpSourceTypeFile' name='wpSourceType' value='file' " .
+                                  "onchange='toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\")' checked='checked' />" .
+                                "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
+                                  "onfocus='" .
+                                    "toggle_element_activation(\"wpUploadFileURL\",\"wpUploadFile\");" .
+                                    "toggle_element_check(\"wpSourceTypeFile\",\"wpSourceTypeURL\")' " .
+                                    "onchange='fillDestFilename(\"wpUploadFile\")' size='60' />" .
+                               wfMsgHTML( 'upload_source_file' ) . "<br/>" .
+                               "<input type='radio' id='wpSourceTypeURL' name='wpSourceType' value='web' " .
+                                 "onchange='toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\")' />" .
+                               "<input tabindex='1' type='text' name='wpUploadFileURL' id='wpUploadFileURL' " .
+                                 "onfocus='" .
+                                   "toggle_element_activation(\"wpUploadFile\",\"wpUploadFileURL\");" .
+                                   "toggle_element_check(\"wpSourceTypeURL\",\"wpSourceTypeFile\")' " .
+                                   "onchange='fillDestFilename(\"wpUploadFileURL\")' size='60' disabled='disabled' />" .
+                               wfMsgHtml( 'upload_source_url' ) ;
+               } else {
+                       $filename_form =
+                               "<input tabindex='1' type='file' name='wpUploadFile' id='wpUploadFile' " .
+                               ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") .
+                               "size='60' />" .
+                               "<input type='hidden' name='wpSourceType' value='file' />" ;
+               }
+               if ( $useAjaxDestCheck ) {
+                       $warningRow = "<tr><td colspan='2' id='wpDestFile-warning'>&nbsp;</td></tr>";
+                       $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"';
+               } else {
+                       $warningRow = '';
+                       $destOnkeyup = '';
+               }
+
+               $encComment = htmlspecialchars( $this->mComment );
+
+               $wgOut->addHTML(
+                        Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL(),
+                                'enctype' => 'multipart/form-data', 'id' => 'mw-upload-form' ) ) .
+                        Xml::openElement( 'fieldset' ) .
+                        Xml::element( 'legend', null, wfMsg( 'upload' ) ) .
+                        Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-upload-table' ) ) .
+                        "<tr>
+                               {$this->uploadFormTextTop}
+                               <td class='mw-label'>
+                                       <label for='wpUploadFile'>{$sourcefilename}</label>
+                               </td>
+                               <td class='mw-input'>
+                                       {$filename_form}
+                               </td>
+                       </tr>
+                       <tr>
+                               <td></td>
+                               <td>
+                                       {$maxUploadSize}
+                                       {$extensionsList}
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class='mw-label'>
+                                       <label for='wpDestFile'>{$destfilename}</label>
+                               </td>
+                               <td class='mw-input'>
+                                       <input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='60'
+                                               value=\"{$encDestName}\" onchange='toggleFilenameFiller()' $destOnkeyup />
+                               </td>
+                       </tr>
+                       <tr>
+                               <td class='mw-label'>
+                                       <label for='wpUploadDescription'>{$summary}</label>
+                               </td>
+                               <td class='mw-input'>
+                                       <textarea tabindex='3' name='wpUploadDescription' id='wpUploadDescription' rows='6'
+                                               cols='{$cols}'{$width}>$encComment</textarea>
+                                       {$this->uploadFormTextAfterSummary}
+                               </td>
+                       </tr>
+                       <tr>"
+               );
+
+               if ( $licenseshtml != '' ) {
+                       global $wgStylePath;
+                       $wgOut->addHTML( "
+                                       <td class='mw-label'>
+                                               <label for='wpLicense'>$license</label>
+                                       </td>
+                                       <td class='mw-input'>
+                                               <select name='wpLicense' id='wpLicense' tabindex='4'
+                                                       onchange='licenseSelectorCheck()'>
+                                                       <option value=''>$nolicense</option>
+                                                       $licenseshtml
+                                               </select>
+                                       </td>
+                               </tr>
+                               <tr>"
+                       );
+                       if( $useAjaxLicensePreview ) {
+                               $wgOut->addHtml( "
+                                               <td></td>
+                                               <td id=\"mw-license-preview\"></td>
+                                       </tr>
+                                       <tr>"
+                               );
+                       }
+               }
+
+               if ( $wgUseCopyrightUpload ) {
+                       $filestatus = wfMsgExt( 'filestatus', 'escapenoentities' );
+                       $copystatus =  htmlspecialchars( $this->mCopyrightStatus );
+                       $filesource = wfMsgExt( 'filesource', 'escapenoentities' );
+                       $uploadsource = htmlspecialchars( $this->mCopyrightSource );
+
+                       $wgOut->addHTML( "
+                                       <td class='mw-label' style='white-space: nowrap;'>
+                                               <label for='wpUploadCopyStatus'>$filestatus</label></td>
+                                       <td class='mw-input'>
+                                               <input tabindex='5' type='text' name='wpUploadCopyStatus' id='wpUploadCopyStatus'
+                                                       value=\"$copystatus\" size='60' />
+                                       </td>
+                               </tr>
+                               <tr>
+                                       <td class='mw-label'>
+                                               <label for='wpUploadCopyStatus'>$filesource</label>
+                                       </td>
+                                       <td class='mw-input'>
+                                               <input tabindex='6' type='text' name='wpUploadSource' id='wpUploadCopyStatus'
+                                                       value=\"$uploadsource\" size='60' />
+                                       </td>
+                               </tr>
+                               <tr>"
+                       );
+               }
+
+               $wgOut->addHtml( "
+                               <td></td>
+                               <td>
+                                       <input tabindex='7' type='checkbox' name='wpWatchthis' id='wpWatchthis' $watchChecked value='true' />
+                                       <label for='wpWatchthis'>" . wfMsgHtml( 'watchthisupload' ) . "</label>
+                                       <input tabindex='8' type='checkbox' name='wpIgnoreWarning' id='wpIgnoreWarning' value='true' $warningChecked/>
+                                       <label for='wpIgnoreWarning'>" . wfMsgHtml( 'ignorewarnings' ) . "</label>
+                               </td>
+                       </tr>
+                       $warningRow
+                       <tr>
+                               <td></td>
+                                       <td class='mw-input'>
+                                               <input tabindex='9' type='submit' name='wpUpload' value=\"{$ulb}\"" . $wgUser->getSkin()->tooltipAndAccesskey( 'upload' ) . " />
+                                       </td>
+                       </tr>
+                       <tr>
+                               <td></td>
+                               <td class='mw-input'>"
+               );
+               $wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
+               $wgOut->addHTML( "
+                               </td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' )
+               );
+               $uploadfooter = wfMsgNoTrans( 'uploadfooter' );
+               if( $uploadfooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadfooter ) ){
+                       $wgOut->addWikiText( '<div id="mw-upload-footer-message">' . $uploadfooter . '</div>' );
+               }
+       }
+
+       /* -------------------------------------------------------------- */
+       
+       /**
+        * See if we should check the 'watch this page' checkbox on the form
+        * based on the user's preferences and whether we're being asked
+        * to create a new file or update an existing one.
+        *
+        * In the case where 'watch edits' is off but 'watch creations' is on,
+        * we'll leave the box unchecked.
+        *
+        * Note that the page target can be changed *on the form*, so our check
+        * state can get out of sync.
+        */
+       function watchCheck() {
+               global $wgUser;
+               if( $wgUser->getOption( 'watchdefault' ) ) {
+                       // Watch all edits!
+                       return true;
+               }
+               
+               $local = wfLocalFile( $this->mDesiredDestName );
+               if( $local && $local->exists() ) {
+                       // We're uploading a new version of an existing file.
+                       // No creation, so don't watch it if we're not already.
+                       return $local->getTitle()->userIsWatching();
+               } else {
+                       // New page should get watched if that's our option.
+                       return $wgUser->getOption( 'watchcreations' );
+               }
+       }
+
+       /**
+        * Split a file into a base name and all dot-delimited 'extensions'
+        * on the end. Some web server configurations will fall back to
+        * earlier pseudo-'extensions' to determine type and execute
+        * scripts, so the blacklist needs to check them all.
+        *
+        * @return array
+        */
+       function splitExtensions( $filename ) {
+               $bits = explode( '.', $filename );
+               $basename = array_shift( $bits );
+               return array( $basename, $bits );
+       }
+
+       /**
+        * Perform case-insensitive match against a list of file extensions.
+        * Returns true if the extension is in the list.
+        *
+        * @param string $ext
+        * @param array $list
+        * @return bool
+        */
+       function checkFileExtension( $ext, $list ) {
+               return in_array( strtolower( $ext ), $list );
+       }
+
+       /**
+        * Perform case-insensitive match against a list of file extensions.
+        * Returns true if any of the extensions are in the list.
+        *
+        * @param array $ext
+        * @param array $list
+        * @return bool
+        */
+       function checkFileExtensionList( $ext, $list ) {
+               foreach( $ext as $e ) {
+                       if( in_array( strtolower( $e ), $list ) ) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Verifies that it's ok to include the uploaded file
+        *
+        * @param string $tmpfile the full path of the temporary file to verify
+        * @param string $extension The filename extension that the file is to be served with
+        * @return mixed true of the file is verified, a WikiError object otherwise.
+        */
+       function verify( $tmpfile, $extension ) {
+               #magically determine mime type
+               $magic = MimeMagic::singleton();
+               $mime = $magic->guessMimeType($tmpfile,false);
+
+               #check mime type, if desired
+               global $wgVerifyMimeType;
+               if ($wgVerifyMimeType) {
+
+                 wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n");
+                       #check mime type against file extension
+                       if( !$this->verifyExtension( $mime, $extension ) ) {
+                               return new WikiErrorMsg( 'uploadcorrupt' );
+                       }
+
+                       #check mime type blacklist
+                       global $wgMimeTypeBlacklist;
+                       if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist)
+                               && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+                               return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
+                       }
+               }
+
+               #check for htmlish code and javascript
+               if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
+                       return new WikiErrorMsg( 'uploadscripted' );
+               }
+
+               /**
+               * Scan the uploaded file for viruses
+               */
+               $virus= $this->detectVirus($tmpfile);
+               if ( $virus ) {
+                       return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) );
+               }
+
+               wfDebug( __METHOD__.": all clear; passing.\n" );
+               return true;
+       }
+
+       /**
+        * Checks if the mime type of the uploaded file matches the file extension.
+        *
+        * @param string $mime the mime type of the uploaded file
+        * @param string $extension The filename extension that the file is to be served with
+        * @return bool
+        */
+       function verifyExtension( $mime, $extension ) {
+               $magic = MimeMagic::singleton();
+
+               if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
+                       if ( ! $magic->isRecognizableExtension( $extension ) ) {
+                               wfDebug( __METHOD__.": passing file with unknown detected mime type; " .
+                                       "unrecognized extension '$extension', can't verify\n" );
+                               return true;
+                       } else {
+                               wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ".
+                                       "recognized extension '$extension', so probably invalid file\n" );
+                               return false;
+                       }
+
+               $match= $magic->isMatchingExtension($extension,$mime);
+
+               if ($match===NULL) {
+                       wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" );
+                       return true;
+               } elseif ($match===true) {
+                       wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" );
+
+                       #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
+                       return true;
+
+               } else {
+                       wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" );
+                       return false;
+               }
+       }
+
+       /**
+        * Heuristic for detecting files that *could* contain JavaScript instructions or
+        * things that may look like HTML to a browser and are thus
+        * potentially harmful. The present implementation will produce false positives in some situations.
+        *
+        * @param string $file Pathname to the temporary upload file
+        * @param string $mime The mime type of the file
+        * @param string $extension The extension of the file
+        * @return bool true if the file contains something looking like embedded scripts
+        */
+       function detectScript($file, $mime, $extension) {
+               global $wgAllowTitlesInSVG;
+
+               #ugly hack: for text files, always look at the entire file.
+               #For binarie field, just check the first K.
+
+               if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file );
+               else {
+                       $fp = fopen( $file, 'rb' );
+                       $chunk = fread( $fp, 1024 );
+                       fclose( $fp );
+               }
+
+               $chunk= strtolower( $chunk );
+
+               if (!$chunk) return false;
+
+               #decode from UTF-16 if needed (could be used for obfuscation).
+               if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE";
+               elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE";
+               else $enc= NULL;
+
+               if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk);
+
+               $chunk= trim($chunk);
+
+               #FIXME: convert from UTF-16 if necessarry!
+
+               wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n");
+
+               #check for HTML doctype
+               if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true;
+
+               /**
+               * Internet Explorer for Windows performs some really stupid file type
+               * autodetection which can cause it to interpret valid image files as HTML
+               * and potentially execute JavaScript, creating a cross-site scripting
+               * attack vectors.
+               *
+               * Apple's Safari browser also performs some unsafe file type autodetection
+               * which can cause legitimate files to be interpreted as HTML if the
+               * web server is not correctly configured to send the right content-type
+               * (or if you're really uploading plain text and octet streams!)
+               *
+               * Returns true if IE is likely to mistake the given file for HTML.
+               * Also returns true if Safari would mistake the given file for HTML
+               * when served with a generic content-type.
+               */
+
+               $tags = array(
+                       '<body',
+                       '<head',
+                       '<html',   #also in safari
+                       '<img',
+                       '<pre',
+                       '<script', #also in safari
+                       '<table'
+                       );
+               if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
+                       $tags[] = '<title';
+               }
+
+               foreach( $tags as $tag ) {
+                       if( false !== strpos( $chunk, $tag ) ) {
+                               return true;
+                       }
+               }
+
+               /*
+               * look for javascript
+               */
+
+               #resolve entity-refs to look at attributes. may be harsh on big files... cache result?
+               $chunk = Sanitizer::decodeCharReferences( $chunk );
+
+               #look for script-types
+               if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true;
+
+               #look for html-style script-urls
+               if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
+
+               #look for css-style script-urls
+               if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true;
+
+               wfDebug("SpecialUpload::detectScript: no scripts found\n");
+               return false;
+       }
+
+       /**
+        * Generic wrapper function for a virus scanner program.
+        * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
+        * $wgAntivirusRequired may be used to deny upload if the scan fails.
+        *
+        * @param string $file Pathname to the temporary upload file
+        * @return mixed false if not virus is found, NULL if the scan fails or is disabled,
+        *         or a string containing feedback from the virus scanner if a virus was found.
+        *         If textual feedback is missing but a virus was found, this function returns true.
+        */
+       function detectVirus($file) {
+               global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut;
+
+               if ( !$wgAntivirus ) {
+                       wfDebug( __METHOD__.": virus scanner disabled\n");
+                       return NULL;
+               }
+
+               if ( !$wgAntivirusSetup[$wgAntivirus] ) {
+                       wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" );
+                       # @TODO: localise
+                       $wgOut->addHTML( "<div class='error'>Bad configuration: unknown virus scanner: <i>$wgAntivirus</i></div>\n" );
+                       return "unknown antivirus: $wgAntivirus";
+               }
+
+               # look up scanner configuration
+               $command = $wgAntivirusSetup[$wgAntivirus]["command"];
+               $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"];
+               $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ?
+                       $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null;
+
+               if ( strpos( $command,"%f" ) === false ) {
+                       # simple pattern: append file to scan
+                       $command .= " " . wfEscapeShellArg( $file );
+               } else {
+                       # complex pattern: replace "%f" with file to scan
+                       $command = str_replace( "%f", wfEscapeShellArg( $file ), $command );
+               }
+
+               wfDebug( __METHOD__.": running virus scan: $command \n" );
+
+               # execute virus scanner
+               $exitCode = false;
+
+               #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
+               #      that does not seem to be worth the pain.
+               #      Ask me (Duesentrieb) about it if it's ever needed.
+               $output = array();
+               if ( wfIsWindows() ) {
+                       exec( "$command", $output, $exitCode );
+               } else {
+                       exec( "$command 2>&1", $output, $exitCode );
+               }
+
+               # map exit code to AV_xxx constants.
+               $mappedCode = $exitCode;
+               if ( $exitCodeMap ) {
+                       if ( isset( $exitCodeMap[$exitCode] ) ) {
+                               $mappedCode = $exitCodeMap[$exitCode];
+                       } elseif ( isset( $exitCodeMap["*"] ) ) {
+                               $mappedCode = $exitCodeMap["*"];
+                       }
+               }
+
+               if ( $mappedCode === AV_SCAN_FAILED ) {
+                       # scan failed (code was mapped to false by $exitCodeMap)
+                       wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" );
+
+                       if ( $wgAntivirusRequired ) {
+                               return "scan failed (code $exitCode)";
+                       } else {
+                               return NULL;
+                       }
+               } else if ( $mappedCode === AV_SCAN_ABORTED ) {
+                       # scan failed because filetype is unknown (probably imune)
+                       wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" );
+                       return NULL;
+               } else if ( $mappedCode === AV_NO_VIRUS ) {
+                       # no virus found
+                       wfDebug( __METHOD__.": file passed virus scan.\n" );
+                       return false;
+               } else {
+                       $output = join( "\n", $output );
+                       $output = trim( $output );
+
+                       if ( !$output ) {
+                               $output = true; #if there's no output, return true
+                       } elseif ( $msgPattern ) {
+                               $groups = array();
+                               if ( preg_match( $msgPattern, $output, $groups ) ) {
+                                       if ( $groups[1] ) {
+                                               $output = $groups[1];
+                                       }
+                               }
+                       }
+
+                       wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" );
+                       return $output;
+               }
+       }
+
+       /**
+        * Check if the temporary file is MacBinary-encoded, as some uploads
+        * from Internet Explorer on Mac OS Classic and Mac OS X will be.
+        * If so, the data fork will be extracted to a second temporary file,
+        * which will then be checked for validity and either kept or discarded.
+        *
+        * @access private
+        */
+       function checkMacBinary() {
+               $macbin = new MacBinary( $this->mTempPath );
+               if( $macbin->isValid() ) {
+                       $dataFile = tempnam( wfTempDir(), "WikiMacBinary" );
+                       $dataHandle = fopen( $dataFile, 'wb' );
+
+                       wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" );
+                       $macbin->extractData( $dataHandle );
+
+                       $this->mTempPath = $dataFile;
+                       $this->mFileSize = $macbin->dataForkLength();
+
+                       // We'll have to manually remove the new file if it's not kept.
+                       $this->mRemoveTempFile = true;
+               }
+               $macbin->close();
+       }
+
+       /**
+        * If we've modified the upload file we need to manually remove it
+        * on exit to clean up.
+        * @access private
+        */
+       function cleanupTempFile() {
+               if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) {
+                       wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
+                       unlink( $this->mTempPath );
+               }
+       }
+
+       /**
+        * Check if there's an overwrite conflict and, if so, if restrictions
+        * forbid this user from performing the upload.
+        *
+        * @return mixed true on success, WikiError on failure
+        * @access private
+        */
+       function checkOverwrite( $name ) {
+               $img = wfFindFile( $name );
+
+               $error = '';
+               if( $img ) {
+                       global $wgUser, $wgOut;
+                       if( $img->isLocal() ) {
+                               if( !self::userCanReUpload( $wgUser, $img->name ) ) {
+                                       $error = 'fileexists-forbidden';
+                               }
+                       } else {
+                               if( !$wgUser->isAllowed( 'reupload' ) ||
+                                   !$wgUser->isAllowed( 'reupload-shared' ) ) {
+                                       $error = "fileexists-shared-forbidden";
+                               }
+                       }
+               }
+
+               if( $error ) {
+                       $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) );
+                       return $errorText;
+               }
+
+               // Rockin', go ahead and upload
+               return true;
+       }
+
+        /**
+        * Check if a user is the last uploader
+        *
+        * @param User $user
+        * @param string $img, image name
+        * @return bool
+        */
+       public static function userCanReUpload( User $user, $img ) {
+               if( $user->isAllowed( 'reupload' ) )
+                       return true; // non-conditional
+               if( !$user->isAllowed( 'reupload-own' ) )
+                       return false;
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $row = $dbr->selectRow('image',
+               /* SELECT */ 'img_user',
+               /* WHERE */ array( 'img_name' => $img )
+               );
+               if ( !$row )
+                       return false;
+
+               return $user->getId() == $row->img_user;
+       }
+
+       /**
+        * Display an error with a wikitext description
+        */
+       function showError( $description ) {
+               global $wgOut;
+               $wgOut->setPageTitle( wfMsg( "internalerror" ) );
+               $wgOut->setRobotpolicy( "noindex,nofollow" );
+               $wgOut->setArticleRelated( false );
+               $wgOut->enableClientCache( false );
+               $wgOut->addWikiText( $description );
+       }
+
+       /**
+        * Get the initial image page text based on a comment and optional file status information
+        */
+       static function getInitialPageText( $comment, $license, $copyStatus, $source ) {
+               global $wgUseCopyrightUpload;
+               if ( $wgUseCopyrightUpload ) {
+                       if ( $license != '' ) {
+                               $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+                       }
+                       $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" .
+                         '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
+                         "$licensetxt" .
+                         '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
+               } else {
+                       if ( $license != '' ) {
+                               $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n";
+                                $pageText = $filedesc .
+                                        '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+                       } else {
+                               $pageText = $comment;
+                       }
+               }
+               return $pageText;
+       }
+
+       /**
+        * If there are rows in the deletion log for this file, show them,
+        * along with a nice little note for the user
+        *
+        * @param OutputPage $out
+        * @param string filename
+        */
+       private function showDeletionLog( $out, $filename ) {
+               global $wgUser;
+               $loglist = new LogEventsList( $wgUser->getSkin(), $out );
+               $pager = new LogPager( $loglist, 'delete', false, $filename );
+               if( $pager->getNumRows() > 0 ) {
+                       $out->addHtml( '<div id="mw-upload-deleted-warn">' );
+                       $out->addWikiMsg( 'upload-wasdeleted' );
+                       $out->addHTML(
+                               $loglist->beginLogEventsList() .
+                               $pager->getBody() .
+                               $loglist->endLogEventsList()
+                       );
+                       $out->addHtml( '</div>' );
+               }
+       }
+}
diff --git a/includes/specials/UploadMogile.php b/includes/specials/UploadMogile.php
new file mode 100644 (file)
index 0000000..7ff8fda
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * You will need the extension MogileClient to use this special page.
+ */
+require_once( 'MogileFS.php' );
+
+/**
+ * Entry point
+ */
+function wfSpecialUploadMogile() {
+       global $wgRequest;
+       $form = new UploadFormMogile( $wgRequest );
+       $form->execute();
+}
+
+/**
+ * Extends Special:Upload with MogileFS.
+ * @ingroup SpecialPage
+ */
+class UploadFormMogile extends UploadForm {
+       /**
+        * Move the uploaded file from its temporary location to the final
+        * destination. If a previous version of the file exists, move
+        * it into the archive subdirectory.
+        *
+        * @todo If the later save fails, we may have disappeared the original file.
+        *
+        * @param string $saveName
+        * @param string $tempName full path to the temporary file
+        * @param bool $useRename  Not used in this implementation
+        */
+       function saveUploadedFile( $saveName, $tempName, $useRename = false ) {
+               global $wgOut;
+               $mfs = MogileFS::NewMogileFS();
+
+               $this->mSavedFile = "image!{$saveName}";
+
+               if( $mfs->getPaths( $this->mSavedFile )) {
+                       $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}";
+                       if( !$mfs->rename( $this->mSavedFile, "archive!{$this->mUploadOldVersion}" ) ) {
+                               $wgOut->showFileRenameError( $this->mSavedFile,
+                                 "archive!{$this->mUploadOldVersion}" );
+                               return false;
+                       }
+               } else {
+                       $this->mUploadOldVersion = '';
+               }
+
+               if ( $this->mStashed ) {
+                       if (!$mfs->rename($tempName,$this->mSavedFile)) {
+                               $wgOut->showFileRenameError($tempName, $this->mSavedFile );
+                               return false;
+                       }
+               } else {
+                       if ( !$mfs->saveFile($this->mSavedFile,'normal',$tempName )) {
+                               $wgOut->showFileCopyError( $tempName, $this->mSavedFile );
+                               return false;
+                       }
+                       unlink($tempName);
+               }
+               return true;
+       }
+
+       /**
+        * Stash a file in a temporary directory for later processing
+        * after the user has confirmed it.
+        *
+        * If the user doesn't explicitly cancel or accept, these files
+        * can accumulate in the temp directory.
+        *
+        * @param string $saveName - the destination filename
+        * @param string $tempName - the source temporary file to save
+        * @return string - full path the stashed file, or false on failure
+        * @access private
+        */
+       function saveTempUploadedFile( $saveName, $tempName ) {
+               global $wgOut;
+
+               $stash = 'stash!' . gmdate( "YmdHis" ) . '!' . $saveName;
+               $mfs = MogileFS::NewMogileFS();
+               if ( !$mfs->saveFile( $stash, 'normal', $tempName ) ) {
+                       $wgOut->showFileCopyError( $tempName, $stash );
+                       return false;
+               }
+               unlink($tempName);
+               return $stash;
+       }
+
+       /**
+        * Stash a file in a temporary directory for later processing,
+        * and save the necessary descriptive info into the session.
+        * Returns a key value which will be passed through a form
+        * to pick up the path info on a later invocation.
+        *
+        * @return int
+        * @access private
+        */
+       function stashSession() {
+               $stash = $this->saveTempUploadedFile(
+                       $this->mUploadSaveName, $this->mUploadTempName );
+
+               if( !$stash ) {
+                       # Couldn't save the file.
+                       return false;
+               }
+
+               $key = mt_rand( 0, 0x7fffffff );
+               $_SESSION['wsUploadData'][$key] = array(
+                       'mUploadTempName' => $stash,
+                       'mUploadSize'     => $this->mUploadSize,
+                       'mOname'          => $this->mOname );
+               return $key;
+       }
+
+       /**
+        * Remove a temporarily kept file stashed by saveTempUploadedFile().
+        * @access private
+        * @return success
+        */
+       function unsaveUploadedFile() {
+               global $wgOut;
+               $mfs = MogileFS::NewMogileFS();
+               if ( ! $mfs->delete( $this->mUploadTempName ) ) {
+                       $wgOut->showFileDeleteError( $this->mUploadTempName );
+                       return false;
+               } else {
+                       return true;
+               }
+       }
+}
diff --git a/includes/specials/Userlogin.php b/includes/specials/Userlogin.php
new file mode 100644 (file)
index 0000000..179ef3f
--- /dev/null
@@ -0,0 +1,928 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogin( $par = '' ) {
+       global $wgRequest;
+       if( session_id() == '' ) {
+               wfSetupSession();
+       }
+
+       $form = new LoginForm( $wgRequest, $par );
+       $form->execute();
+}
+
+/**
+ * implements Special:Login
+ * @ingroup SpecialPage
+ */
+class LoginForm {
+
+       const SUCCESS = 0;
+       const NO_NAME = 1;
+       const ILLEGAL = 2;
+       const WRONG_PLUGIN_PASS = 3;
+       const NOT_EXISTS = 4;
+       const WRONG_PASS = 5;
+       const EMPTY_PASS = 6;
+       const RESET_PASS = 7;
+       const ABORTED = 8;
+       const CREATE_BLOCKED = 9;
+
+       var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
+       var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
+       var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage, $mSkipCookieCheck;
+
+       /**
+        * Constructor
+        * @param WebRequest $request A WebRequest object passed by reference
+        */
+       function LoginForm( &$request, $par = '' ) {
+               global $wgLang, $wgAllowRealName, $wgEnableEmail;
+               global $wgAuth;
+
+               $this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]]
+               $this->mName = $request->getText( 'wpName' );
+               $this->mPassword = $request->getText( 'wpPassword' );
+               $this->mRetype = $request->getText( 'wpRetype' );
+               $this->mDomain = $request->getText( 'wpDomain' );
+               $this->mReturnTo = $request->getVal( 'returnto' );
+               $this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
+               $this->mPosted = $request->wasPosted();
+               $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
+               $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' )
+                                           && $wgEnableEmail;
+               $this->mMailmypassword = $request->getCheck( 'wpMailmypassword' )
+                                        && $wgEnableEmail;
+               $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' );
+               $this->mAction = $request->getVal( 'action' );
+               $this->mRemember = $request->getCheck( 'wpRemember' );
+               $this->mLanguage = $request->getText( 'uselang' );
+               $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' );
+
+               if( $wgEnableEmail ) {
+                       $this->mEmail = $request->getText( 'wpEmail' );
+               } else {
+                       $this->mEmail = '';
+               }
+               if( $wgAllowRealName ) {
+                   $this->mRealName = $request->getText( 'wpRealName' );
+               } else {
+                   $this->mRealName = '';
+               }
+
+               if( !$wgAuth->validDomain( $this->mDomain ) ) {
+                       $this->mDomain = 'invaliddomain';
+               }
+               $wgAuth->setDomain( $this->mDomain );
+
+               # When switching accounts, it sucks to get automatically logged out
+               if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) {
+                       $this->mReturnTo = '';
+               }
+       }
+
+       function execute() {
+               if ( !is_null( $this->mCookieCheck ) ) {
+                       $this->onCookieRedirectCheck( $this->mCookieCheck );
+                       return;
+               } else if( $this->mPosted ) {
+                       if( $this->mCreateaccount ) {
+                               return $this->addNewAccount();
+                       } else if ( $this->mCreateaccountMail ) {
+                               return $this->addNewAccountMailPassword();
+                       } else if ( $this->mMailmypassword ) {
+                               return $this->mailPassword();
+                       } else if ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) {
+                               return $this->processLogin();
+                       }
+               }
+               $this->mainLoginForm( '' );
+       }
+
+       /**
+        * @private
+        */
+       function addNewAccountMailPassword() {
+               global $wgOut;
+
+               if ('' == $this->mEmail) {
+                       $this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) );
+                       return;
+               }
+
+               $u = $this->addNewaccountInternal();
+
+               if ($u == NULL) {
+                       return;
+               }
+
+               // Wipe the initial password and mail a temporary one
+               $u->setPassword( null );
+               $u->saveSettings();
+               $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' );
+
+               wfRunHooks( 'AddNewAccount', array( $u, true ) );
+
+               $wgOut->setPageTitle( wfMsg( 'accmailtitle' ) );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+               $wgOut->setArticleRelated( false );
+
+               if( WikiError::isError( $result ) ) {
+                       $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+               } else {
+                       $wgOut->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() );
+                       $wgOut->returnToMain( false );
+               }
+               $u = 0;
+       }
+
+
+       /**
+        * @private
+        */
+       function addNewAccount() {
+               global $wgUser, $wgEmailAuthentication;
+
+               # Create the account and abort if there's a problem doing so
+               $u = $this->addNewAccountInternal();
+               if( $u == NULL )
+                       return;
+
+               # If we showed up language selection links, and one was in use, be
+               # smart (and sensible) and save that language as the user's preference
+               global $wgLoginLanguageSelector;
+               if( $wgLoginLanguageSelector && $this->mLanguage )
+                       $u->setOption( 'language', $this->mLanguage );
+
+               # Send out an email authentication message if needed
+               if( $wgEmailAuthentication && User::isValidEmailAddr( $u->getEmail() ) ) {
+                       global $wgOut;
+                       $error = $u->sendConfirmationMail();
+                       if( WikiError::isError( $error ) ) {
+                               $wgOut->addWikiMsg( 'confirmemail_sendfailed', $error->getMessage() );
+                       } else {
+                               $wgOut->addWikiMsg( 'confirmemail_oncreate' );
+                       }
+               }
+
+               # Save settings (including confirmation token)
+               $u->saveSettings();
+
+               # If not logged in, assume the new account as the current one and set session cookies
+               # then show a "welcome" message or a "need cookies" message as needed
+               if( $wgUser->isAnon() ) {
+                       $wgUser = $u;
+                       $wgUser->setCookies();
+                       wfRunHooks( 'AddNewAccount', array( $wgUser ) );
+                       if( $this->hasSessionCookie() ) {
+                               return $this->successfulLogin( wfMsg( 'welcomecreation', $wgUser->getName() ), false );
+                       } else {
+                               return $this->cookieRedirectCheck( 'new' );
+                       }
+               } else {
+                       # Confirm that the account was created
+                       global $wgOut;
+                       $self = SpecialPage::getTitleFor( 'Userlogin' );
+                       $wgOut->setPageTitle( wfMsgHtml( 'accountcreated' ) );
+                       $wgOut->setArticleRelated( false );
+                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
+                       $wgOut->addHtml( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
+                       $wgOut->returnToMain( false, $self );
+                       wfRunHooks( 'AddNewAccount', array( $u ) );
+                       return true;
+               }
+       }
+
+       /**
+        * @private
+        */
+       function addNewAccountInternal() {
+               global $wgUser, $wgOut;
+               global $wgEnableSorbs, $wgProxyWhitelist;
+               global $wgMemc, $wgAccountCreationThrottle;
+               global $wgAuth, $wgMinimalPasswordLength;
+               global $wgEmailConfirmToEdit;
+
+               // If the user passes an invalid domain, something is fishy
+               if( !$wgAuth->validDomain( $this->mDomain ) ) {
+                       $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+                       return false;
+               }
+
+               // If we are not allowing users to login locally, we should
+               // be checking to see if the user is actually able to
+               // authenticate to the authentication server before they
+               // create an account (otherwise, they can create a local account
+               // and login as any domain user). We only need to check this for
+               // domains that aren't local.
+               if( 'local' != $this->mDomain && '' != $this->mDomain ) {
+                       if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
+                               $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+                               return false;
+                       }
+               }
+
+               if ( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return false;
+               }
+
+               # Check permissions
+               if ( !$wgUser->isAllowed( 'createaccount' ) ) {
+                       $this->userNotPrivilegedMessage();
+                       return false;
+               } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+                       $this->userBlockedMessage();
+                       return false;
+               }
+
+               $ip = wfGetIP();
+               if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
+                 $wgUser->inSorbsBlacklist( $ip ) )
+               {
+                       $this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' );
+                       return;
+               }
+
+               # Now create a dummy user ($u) and check if it is valid
+               $name = trim( $this->mName );
+               $u = User::newFromName( $name, 'creatable' );
+               if ( is_null( $u ) ) {
+                       $this->mainLoginForm( wfMsg( 'noname' ) );
+                       return false;
+               }
+
+               if ( 0 != $u->idForName() ) {
+                       $this->mainLoginForm( wfMsg( 'userexists' ) );
+                       return false;
+               }
+
+               if ( 0 != strcmp( $this->mPassword, $this->mRetype ) ) {
+                       $this->mainLoginForm( wfMsg( 'badretype' ) );
+                       return false;
+               }
+
+               # check for minimal password length
+               if ( !$u->isValidPassword( $this->mPassword ) ) {
+                       if ( !$this->mCreateaccountMail ) {
+                               $this->mainLoginForm( wfMsgExt( 'passwordtooshort', array( 'parsemag' ), $wgMinimalPasswordLength ) );
+                               return false;
+                       } else {
+                               # do not force a password for account creation by email
+                               # set invalid password, it will be replaced later by a random generated password
+                               $this->mPassword = null;
+                       }
+               }
+
+               # if you need a confirmed email address to edit, then obviously you need an email address.
+               if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) ) {
+                       $this->mainLoginForm( wfMsg( 'noemailtitle' ) );
+                       return false;
+               }
+
+               if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
+                       $this->mainLoginForm( wfMsg( 'invalidemailaddress' ) );
+                       return false;
+               }
+
+               # Set some additional data so the AbortNewAccount hook can be
+               # used for more than just username validation
+               $u->setEmail( $this->mEmail );
+               $u->setRealName( $this->mRealName );
+
+               $abortError = '';
+               if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) {
+                       // Hook point to add extra creation throttles and blocks
+                       wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
+                       $this->mainLoginForm( $abortError );
+                       return false;
+               }
+
+               if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
+                       $key = wfMemcKey( 'acctcreate', 'ip', $ip );
+                       $value = $wgMemc->incr( $key );
+                       if ( !$value ) {
+                               $wgMemc->set( $key, 1, 86400 );
+                       }
+                       if ( $value > $wgAccountCreationThrottle ) {
+                               $this->throttleHit( $wgAccountCreationThrottle );
+                               return false;
+                       }
+               }
+
+               if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
+                       $this->mainLoginForm( wfMsg( 'externaldberror' ) );
+                       return false;
+               }
+
+               return $this->initUser( $u, false );
+       }
+
+       /**
+        * Actually add a user to the database.
+        * Give it a User object that has been initialised with a name.
+        *
+        * @param $u User object.
+        * @param $autocreate boolean -- true if this is an autocreation via auth plugin
+        * @return User object.
+        * @private
+        */
+       function initUser( $u, $autocreate ) {
+               global $wgAuth;
+
+               $u->addToDatabase();
+
+               if ( $wgAuth->allowPasswordChange() ) {
+                       $u->setPassword( $this->mPassword );
+               }
+
+               $u->setEmail( $this->mEmail );
+               $u->setRealName( $this->mRealName );
+               $u->setToken();
+
+               $wgAuth->initUser( $u, $autocreate );
+
+               $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+               $u->saveSettings();
+
+               # Update user count
+               $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+               $ssUpdate->doUpdate();
+
+               return $u;
+       }
+
+       /**
+        * Internally authenticate the login request.
+        *
+        * This may create a local account as a side effect if the
+        * authentication plugin allows transparent local account
+        * creation.
+        *
+        * @public
+        */
+       function authenticateUserData() {
+               global $wgUser, $wgAuth;
+               if ( '' == $this->mName ) {
+                       return self::NO_NAME;
+               }
+
+               // Load $wgUser now, and check to see if we're logging in as the same name. 
+               // This is necessary because loading $wgUser (say by calling getName()) calls
+               // the UserLoadFromSession hook, which potentially creates the user in the 
+               // database. Until we load $wgUser, checking for user existence using 
+               // User::newFromName($name)->getId() below will effectively be using stale data.
+               if ( $wgUser->getName() === $this->mName ) {
+                       wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
+                       return self::SUCCESS;
+               }
+               $u = User::newFromName( $this->mName );
+               if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) {
+                       return self::ILLEGAL;
+               }
+
+               $isAutoCreated = false;
+               if ( 0 == $u->getID() ) {
+                       $status = $this->attemptAutoCreate( $u );
+                       if ( $status !== self::SUCCESS ) {
+                               return $status;
+                       } else {
+                               $isAutoCreated = true;
+                       }
+               } else {
+                       $u->load();
+               }
+
+               // Give general extensions, such as a captcha, a chance to abort logins
+               $abort = self::ABORTED;
+               if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort ) ) ) {
+                       return $abort;
+               }
+
+               if (!$u->checkPassword( $this->mPassword )) {
+                       if( $u->checkTemporaryPassword( $this->mPassword ) ) {
+                               // The e-mailed temporary password should not be used
+                               // for actual logins; that's a very sloppy habit,
+                               // and insecure if an attacker has a few seconds to
+                               // click "search" on someone's open mail reader.
+                               //
+                               // Allow it to be used only to reset the password
+                               // a single time to a new value, which won't be in
+                               // the user's e-mail archives.
+                               //
+                               // For backwards compatibility, we'll still recognize
+                               // it at the login form to minimize surprises for
+                               // people who have been logging in with a temporary
+                               // password for some time.
+                               //
+                               // As a side-effect, we can authenticate the user's
+                               // e-mail address if it's not already done, since
+                               // the temporary password was sent via e-mail.
+                               //
+                               if( !$u->isEmailConfirmed() ) {
+                                       $u->confirmEmail();
+                                       $u->saveSettings();
+                               }
+
+                               // At this point we just return an appropriate code
+                               // indicating that the UI should show a password
+                               // reset form; bot interfaces etc will probably just
+                               // fail cleanly here.
+                               //
+                               $retval = self::RESET_PASS;
+                       } else {
+                               $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+                       }
+               } else {
+                       $wgAuth->updateUser( $u );
+                       $wgUser = $u;
+
+                       if ( $isAutoCreated ) {
+                               // Must be run after $wgUser is set, for correct new user log
+                               wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
+                       }
+
+                       $retval = self::SUCCESS;
+               }
+               wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) );
+               return $retval;
+       }
+
+       /**
+        * Attempt to automatically create a user on login.
+        * Only succeeds if there is an external authentication method which allows it.
+        * @return integer Status code
+        */
+       function attemptAutoCreate( $user ) {
+               global $wgAuth, $wgUser;
+               /**
+                * If the external authentication plugin allows it,
+                * automatically create a new account for users that
+                * are externally defined but have not yet logged in.
+                */
+               if ( !$wgAuth->autoCreate() ) {
+                       return self::NOT_EXISTS;
+               }
+               if ( !$wgAuth->userExists( $user->getName() ) ) {
+                       wfDebug( __METHOD__.": user does not exist\n" );
+                       return self::NOT_EXISTS;
+               }
+               if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
+                       wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
+                       return self::WRONG_PLUGIN_PASS;
+               }
+               if ( $wgUser->isBlockedFromCreateAccount() ) {
+                       wfDebug( __METHOD__.": user is blocked from account creation\n" );
+                       return self::CREATE_BLOCKED;
+               }
+
+               wfDebug( __METHOD__.": creating account\n" );
+               $user = $this->initUser( $user, true );
+               return self::SUCCESS;
+       }
+
+       function processLogin() {
+               global $wgUser, $wgAuth;
+
+               switch ($this->authenticateUserData())
+               {
+                       case self::SUCCESS:
+                               # We've verified now, update the real record
+                               if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
+                                       $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+                                       $wgUser->saveSettings();
+                               } else {
+                                       $wgUser->invalidateCache();
+                               }
+                               $wgUser->setCookies();
+
+                               if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
+                                       /* Replace the language object to provide user interface in correct
+                                        * language immediately on this first page load.
+                                        */
+                                       global $wgLang, $wgRequest;
+                                       $code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) );
+                                       $wgLang = Language::factory( $code );
+                                       return $this->successfulLogin( wfMsg( 'loginsuccess', $wgUser->getName() ) );
+                               } else {
+                                       return $this->cookieRedirectCheck( 'login' );
+                               }
+                               break;
+
+                       case self::NO_NAME:
+                       case self::ILLEGAL:
+                               $this->mainLoginForm( wfMsg( 'noname' ) );
+                               break;
+                       case self::WRONG_PLUGIN_PASS:
+                               $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+                               break;
+                       case self::NOT_EXISTS:
+                               if( $wgUser->isAllowed( 'createaccount' ) ){
+                                       $this->mainLoginForm( wfMsg( 'nosuchuser', htmlspecialchars( $this->mName ) ) );
+                               } else {
+                                       $this->mainLoginForm( wfMsg( 'nosuchusershort', htmlspecialchars( $this->mName ) ) );
+                               }
+                               break;
+                       case self::WRONG_PASS:
+                               $this->mainLoginForm( wfMsg( 'wrongpassword' ) );
+                               break;
+                       case self::EMPTY_PASS:
+                               $this->mainLoginForm( wfMsg( 'wrongpasswordempty' ) );
+                               break;
+                       case self::RESET_PASS:
+                               $this->resetLoginForm( wfMsg( 'resetpass_announce' ) );
+                               break;
+                       case self::CREATE_BLOCKED:
+                               $this->userBlockedMessage();
+                               break;
+                       default:
+                               throw new MWException( "Unhandled case value" );
+               }
+       }
+
+       function resetLoginForm( $error ) {
+               global $wgOut;
+               $wgOut->addWikiText( "<div class=\"errorbox\">$error</div>" );
+               $reset = new PasswordResetForm( $this->mName, $this->mPassword );
+               $reset->execute( null );
+       }
+
+       /**
+        * @private
+        */
+       function mailPassword() {
+               global $wgUser, $wgOut, $wgAuth;
+
+               if( !$wgAuth->allowPasswordChange() ) {
+                       $this->mainLoginForm( wfMsg( 'resetpass_forbidden' ) );
+                       return;
+               }
+
+               # Check against blocked IPs
+               # fixme -- should we not?
+               if( $wgUser->isBlocked() ) {
+                       $this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
+                       return;
+               }
+
+               # Check against the rate limiter
+               if( $wgUser->pingLimiter( 'mailpassword' ) ) {
+                       $wgOut->rateLimited();
+                       return;
+               }
+
+               if ( '' == $this->mName ) {
+                       $this->mainLoginForm( wfMsg( 'noname' ) );
+                       return;
+               }
+               $u = User::newFromName( $this->mName );
+               if( is_null( $u ) ) {
+                       $this->mainLoginForm( wfMsg( 'noname' ) );
+                       return;
+               }
+               if ( 0 == $u->getID() ) {
+                       $this->mainLoginForm( wfMsg( 'nosuchuser', $u->getName() ) );
+                       return;
+               }
+
+               # Check against password throttle
+               if ( $u->isPasswordReminderThrottled() ) {
+                       global $wgPasswordReminderResendTime;
+                       # Round the time in hours to 3 d.p., in case someone is specifying minutes or seconds.
+                       $this->mainLoginForm( wfMsgExt( 'throttled-mailpassword', array( 'parsemag' ),
+                               round( $wgPasswordReminderResendTime, 3 ) ) );
+                       return;
+               }
+
+               $result = $this->mailPasswordInternal( $u, true, 'passwordremindertitle', 'passwordremindertext' );
+               if( WikiError::isError( $result ) ) {
+                       $this->mainLoginForm( wfMsg( 'mailerror', $result->getMessage() ) );
+               } else {
+                       $this->mainLoginForm( wfMsg( 'passwordsent', $u->getName() ), 'success' );
+               }
+       }
+
+
+       /**
+        * @param object user
+        * @param bool throttle
+        * @param string message name of email title
+        * @param string message name of email text
+        * @return mixed true on success, WikiError on failure
+        * @private
+        */
+       function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) {
+               global $wgCookiePath, $wgCookieDomain, $wgCookiePrefix, $wgCookieSecure;
+               global $wgServer, $wgScript;
+
+               if ( '' == $u->getEmail() ) {
+                       return new WikiError( wfMsg( 'noemail', $u->getName() ) );
+               }
+
+               $np = $u->randomPassword();
+               $u->setNewpassword( $np, $throttle );
+               $u->saveSettings();
+
+               $ip = wfGetIP();
+               if ( '' == $ip ) { $ip = '(Unknown)'; }
+
+               $m = wfMsg( $emailText, $ip, $u->getName(), $np, $wgServer . $wgScript );
+               $result = $u->sendMail( wfMsg( $emailTitle ), $m );
+
+               return $result;
+       }
+
+
+       /**
+        * @param string $msg Message that will be shown on success
+        * @param bool $auto Toggle auto-redirect to main page; default true
+        * @private
+        */
+       function successfulLogin( $msg, $auto = true ) {
+               global $wgUser;
+               global $wgOut;
+
+               # Run any hooks; ignore results
+
+               $injected_html = '';
+               wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
+
+               $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+               $wgOut->setArticleRelated( false );
+               $wgOut->addWikiText( $msg );
+               $wgOut->addHtml( $injected_html );
+               if ( !empty( $this->mReturnTo ) ) {
+                       $wgOut->returnToMain( $auto, $this->mReturnTo );
+               } else {
+                       $wgOut->returnToMain( $auto );
+               }
+       }
+
+       /** */
+       function userNotPrivilegedMessage($errors) {
+               global $wgOut;
+
+               $wgOut->setPageTitle( wfMsg( 'permissionserrors' ) );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+               $wgOut->setArticleRelated( false );
+
+               $wgOut->addWikitext( $wgOut->formatPermissionsErrorMessage( $errors, 'createaccount' ) );
+               // Stuff that might want to be added at the end. For example, instructions if blocked.
+               $wgOut->addWikiMsg( 'cantcreateaccount-nonblock-text' );
+
+               $wgOut->returnToMain( false );
+       }
+
+       /** */
+       function userBlockedMessage() {
+               global $wgOut, $wgUser;
+
+               # Let's be nice about this, it's likely that this feature will be used
+               # for blocking large numbers of innocent people, e.g. range blocks on
+               # schools. Don't blame it on the user. There's a small chance that it
+               # really is the user's fault, i.e. the username is blocked and they
+               # haven't bothered to log out before trying to create an account to
+               # evade it, but we'll leave that to their guilty conscience to figure
+               # out.
+
+               $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+               $wgOut->setArticleRelated( false );
+
+               $ip = wfGetIP();
+               $blocker = User::whoIs( $wgUser->mBlock->mBy );
+               $block_reason = $wgUser->mBlock->mReason;
+
+               if ( strval( $block_reason ) === '' ) {
+                       $block_reason = wfMsg( 'blockednoreason' );
+               }
+               $wgOut->addWikiMsg( 'cantcreateaccount-text', $ip, $block_reason, $blocker );
+               $wgOut->returnToMain( false );
+       }
+
+       /**
+        * @private
+        */
+       function mainLoginForm( $msg, $msgtype = 'error' ) {
+               global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
+               global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector;
+               global $wgAuth, $wgEmailConfirmToEdit;
+               
+               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+               
+               if ( $this->mType == 'signup' ) {
+                       // Block signup here if in readonly. Keeps user from 
+                       // going through the process (filling out data, etc) 
+                       // and being informed later.
+                       if ( wfReadOnly() ) {
+                               $wgOut->readOnlyPage();
+                               return;
+                       } elseif ( $wgUser->isBlockedFromCreateAccount() ) {
+                               $this->userBlockedMessage();
+                               return;
+                       } elseif ( count( $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $wgUser, true ) )>0 ) {
+                               $wgOut->showPermissionsErrorPage( $permErrors, 'createaccount' );
+                               return;
+                       }
+               }
+
+               if ( '' == $this->mName ) {
+                       if ( $wgUser->isLoggedIn() ) {
+                               $this->mName = $wgUser->getName();
+                       } else {
+                               $this->mName = isset( $_COOKIE[$wgCookiePrefix.'UserName'] ) ? $_COOKIE[$wgCookiePrefix.'UserName'] : null;
+                       }
+               }
+
+               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+
+               if ( $this->mType == 'signup' ) {
+                       $template = new UsercreateTemplate();
+                       $q = 'action=submitlogin&type=signup';
+                       $linkq = 'type=login';
+                       $linkmsg = 'gotaccount';
+               } else {
+                       $template = new UserloginTemplate();
+                       $q = 'action=submitlogin&type=login';
+                       $linkq = 'type=signup';
+                       $linkmsg = 'nologin';
+               }
+
+               if ( !empty( $this->mReturnTo ) ) {
+                       $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+                       $q .= $returnto;
+                       $linkq .= $returnto;
+               }
+
+               # Pass any language selection on to the mode switch link
+               if( $wgLoginLanguageSelector && $this->mLanguage )
+                       $linkq .= '&uselang=' . $this->mLanguage;
+
+               $link = '<a href="' . htmlspecialchars ( $titleObj->getLocalUrl( $linkq ) ) . '">';
+               $link .= wfMsgHtml( $linkmsg . 'link' ); # Calling either 'gotaccountlink' or 'nologinlink'
+               $link .= '</a>';
+
+               # Don't show a "create account" link if the user can't
+               if( $this->showCreateOrLoginLink( $wgUser ) )
+                       $template->set( 'link', wfMsgHtml( $linkmsg, $link ) );
+               else
+                       $template->set( 'link', '' );
+
+               $template->set( 'header', '' );
+               $template->set( 'name', $this->mName );
+               $template->set( 'password', $this->mPassword );
+               $template->set( 'retype', $this->mRetype );
+               $template->set( 'email', $this->mEmail );
+               $template->set( 'realname', $this->mRealName );
+               $template->set( 'domain', $this->mDomain );
+
+               $template->set( 'action', $titleObj->getLocalUrl( $q ) );
+               $template->set( 'message', $msg );
+               $template->set( 'messagetype', $msgtype );
+               $template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
+               $template->set( 'userealname', $wgAllowRealName );
+               $template->set( 'useemail', $wgEnableEmail );
+               $template->set( 'emailrequired', $wgEmailConfirmToEdit );
+               $template->set( 'canreset', $wgAuth->allowPasswordChange() );
+               $template->set( 'remember', $wgUser->getOption( 'rememberpassword' ) or $this->mRemember  );
+
+               # Prepare language selection links as needed
+               if( $wgLoginLanguageSelector ) {
+                       $template->set( 'languages', $this->makeLanguageSelector() );
+                       if( $this->mLanguage )
+                               $template->set( 'uselang', $this->mLanguage );
+               }
+
+               // Give authentication and captcha plugins a chance to modify the form
+               $wgAuth->modifyUITemplate( $template );
+               if ( $this->mType == 'signup' ) {
+                       wfRunHooks( 'UserCreateForm', array( &$template ) );
+               } else {
+                       wfRunHooks( 'UserLoginForm', array( &$template ) );
+               }
+
+               $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
+               $wgOut->setRobotpolicy( 'noindex,nofollow' );
+               $wgOut->setArticleRelated( false );
+               $wgOut->disallowUserJs();  // just in case...
+               $wgOut->addTemplate( $template );
+       }
+
+       /**
+        * @private
+        */
+       function showCreateOrLoginLink( &$user ) {
+               if( $this->mType == 'signup' ) {
+                       return( true );
+               } elseif( $user->isAllowed( 'createaccount' ) ) {
+                       return( true );
+               } else {
+                       return( false );
+               }
+       }
+
+       /**
+        * Check if a session cookie is present.
+        *
+        * This will not pick up a cookie set during _this_ request, but is
+        * meant to ensure that the client is returning the cookie which was
+        * set on a previous pass through the system.
+        *
+        * @private
+        */
+       function hasSessionCookie() {
+               global $wgDisableCookieCheck, $wgRequest;
+               return $wgDisableCookieCheck ? true : $wgRequest->checkSessionCookie();
+       }
+
+       /**
+        * @private
+        */
+       function cookieRedirectCheck( $type ) {
+               global $wgOut;
+
+               $titleObj = SpecialPage::getTitleFor( 'Userlogin' );
+               $check = $titleObj->getFullURL( 'wpCookieCheck='.$type );
+
+               return $wgOut->redirect( $check );
+       }
+
+       /**
+        * @private
+        */
+       function onCookieRedirectCheck( $type ) {
+               global $wgUser;
+
+               if ( !$this->hasSessionCookie() ) {
+                       if ( $type == 'new' ) {
+                               return $this->mainLoginForm( wfMsgExt( 'nocookiesnew', array( 'parseinline' ) ) );
+                       } else if ( $type == 'login' ) {
+                               return $this->mainLoginForm( wfMsgExt( 'nocookieslogin', array( 'parseinline' ) ) );
+                       } else {
+                               # shouldn't happen
+                               return $this->mainLoginForm( wfMsg( 'error' ) );
+                       }
+               } else {
+                       return $this->successfulLogin( wfMsgExt( 'loginsuccess', array( 'parseinline' ), $wgUser->getName() ) );
+               }
+       }
+
+       /**
+        * @private
+        */
+       function throttleHit( $limit ) {
+               global $wgOut;
+
+               $wgOut->addWikiMsg( 'acct_creation_throttle_hit', $limit );
+       }
+
+       /**
+        * Produce a bar of links which allow the user to select another language
+        * during login/registration but retain "returnto"
+        *
+        * @return string
+        */
+       function makeLanguageSelector() {
+               $msg = wfMsgForContent( 'loginlanguagelinks' );
+               if( $msg != '' && !wfEmptyMsg( 'loginlanguagelinks', $msg ) ) {
+                       $langs = explode( "\n", $msg );
+                       $links = array();
+                       foreach( $langs as $lang ) {
+                               $lang = trim( $lang, '* ' );
+                               $parts = explode( '|', $lang );
+                               if (count($parts) >= 2) {
+                                       $links[] = $this->makeLanguageSelectorLink( $parts[0], $parts[1] );
+                               }
+                       }
+                       return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', implode( ' | ', $links ) ) : '';
+               } else {
+                       return '';
+               }
+       }
+
+       /**
+        * Create a language selector link for a particular language
+        * Links back to this page preserving type and returnto
+        *
+        * @param $text Link text
+        * @param $lang Language code
+        */
+       function makeLanguageSelectorLink( $text, $lang ) {
+               global $wgUser;
+               $self = SpecialPage::getTitleFor( 'Userlogin' );
+               $attr[] = 'uselang=' . $lang;
+               if( $this->mType == 'signup' )
+                       $attr[] = 'type=signup';
+               if( $this->mReturnTo )
+                       $attr[] = 'returnto=' . $this->mReturnTo;
+               $skin = $wgUser->getSkin();
+               return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
+       }
+}
diff --git a/includes/specials/Userlogout.php b/includes/specials/Userlogout.php
new file mode 100644 (file)
index 0000000..137eadb
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialUserlogout() {
+       global $wgUser, $wgOut;
+
+       $oldName = $wgUser->getName();
+       $wgUser->logout();
+       $wgOut->setRobotpolicy( 'noindex,nofollow' );
+
+       // Hook.
+       $injected_html = '';
+       wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) );
+
+       $wgOut->addHTML( wfMsgExt( 'logouttext', array( 'parse' ) ) . $injected_html );
+       $wgOut->returnToMain();
+}
diff --git a/includes/specials/Userrights.php b/includes/specials/Userrights.php
new file mode 100644 (file)
index 0000000..bf4d440
--- /dev/null
@@ -0,0 +1,576 @@
+<?php
+/**
+ * Special page to allow managing user group membership
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A class to manage user levels rights.
+ * @ingroup SpecialPage
+ */
+class UserrightsPage extends SpecialPage {
+       # The target of the local right-adjuster's interest.  Can be gotten from
+       # either a GET parameter or a subpage-style parameter, so have a member
+       # variable for it.
+       protected $mTarget;
+       protected $isself = false;
+
+       public function __construct() {
+               parent::__construct( 'Userrights' );
+       }
+
+       public function isRestricted() {
+               return true;
+       }
+
+       public function userCanExecute( $user ) {
+               $available = $this->changeableGroups();
+               return !empty( $available['add'] )
+                       or !empty( $available['remove'] )
+                       or ($this->isself and
+                               (!empty( $available['add-self'] )
+                                or !empty( $available['remove-self'] )));
+       }
+
+       /**
+        * Manage forms to be shown according to posted data.
+        * Depending on the submit button used, call a form or a save function.
+        *
+        * @param $par Mixed: string if any subpage provided, else null
+        */
+       function execute( $par ) {
+               // If the visitor doesn't have permissions to assign or remove
+               // any groups, it's a bit silly to give them the user search prompt.
+               global $wgUser, $wgRequest;
+
+               if( $par ) {
+                       $this->mTarget = $par;
+               } else {
+                       $this->mTarget = $wgRequest->getVal( 'user' );
+               }
+
+               if (!$this->mTarget) {
+                       /*
+                        * If the user specified no target, and they can only
+                        * edit their own groups, automatically set them as the
+                        * target.
+                        */
+                       $available = $this->changeableGroups();
+                       if (empty($available['add']) && empty($available['remove']))
+                               $this->mTarget = $wgUser->getName();
+               }
+
+               if ($this->mTarget == $wgUser->getName())
+                       $this->isself = true;
+
+               if( !$this->userCanExecute( $wgUser ) ) {
+                       // fixme... there may be intermediate groups we can mention.
+                       global $wgOut;
+                       $wgOut->showPermissionsErrorPage( array(
+                               $wgUser->isAnon()
+                                       ? 'userrights-nologin'
+                                       : 'userrights-notallowed' ) );
+                       return;
+               }
+
+               if ( wfReadOnly() ) {
+                       global $wgOut;
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+
+               $this->outputHeader();
+
+               $this->setHeaders();
+
+               // show the general form
+               $this->switchForm();
+
+               if( $wgRequest->wasPosted() ) {
+                       // save settings
+                       if( $wgRequest->getCheck( 'saveusergroups' ) ) {
+                               $reason = $wgRequest->getVal( 'user-reason' );
+                               if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $this->mTarget ) ) {
+                                       $this->saveUserGroups(
+                                               $this->mTarget,
+                                               $reason
+                                       );
+                               }
+                       }
+               }
+
+               // show some more forms
+               if( $this->mTarget ) {
+                       $this->editUserGroupsForm( $this->mTarget );
+               }
+       }
+
+       /**
+        * Save user groups changes in the database.
+        * Data comes from the editUserGroupsForm() form function
+        *
+        * @param $username String: username to apply changes to.
+        * @param $reason String: reason for group change
+        * @return null
+        */
+       function saveUserGroups( $username, $reason = '') {
+               global $wgRequest, $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+               $user = $this->fetchUser( $username );
+               if( !$user ) {
+                       return;
+               }
+
+               $allgroups = $this->getAllGroups();
+               $addgroup = array();
+               $removegroup = array();
+
+               // This could possibly create a highly unlikely race condition if permissions are changed between
+               //  when the form is loaded and when the form is saved. Ignoring it for the moment.
+               foreach ($allgroups as $group) {
+                       // We'll tell it to remove all unchecked groups, and add all checked groups.
+                       // Later on, this gets filtered for what can actually be removed
+                       if ($wgRequest->getCheck( "wpGroup-$group" )) {
+                               $addgroup[] = $group;
+                       } else {
+                               $removegroup[] = $group;
+                       }
+               }
+
+               // Validate input set...
+               $changeable = $this->changeableGroups();
+               if ($wgUser->getId() != 0 && $wgUser->getId() == $user->getId()) {
+                       $addable = array_merge($changeable['add'], $wgGroupsAddToSelf);
+                       $removable = array_merge($changeable['remove'], $wgGroupsRemoveFromSelf);
+               } else {
+                       $addable = $changeable['add'];
+                       $removable = $changeable['remove'];
+               }
+
+               $removegroup = array_unique(
+                       array_intersect( (array)$removegroup, $removable ) );
+               $addgroup = array_unique(
+                       array_intersect( (array)$addgroup, $addable ) );
+
+               $oldGroups = $user->getGroups();
+               $newGroups = $oldGroups;
+               // remove then add groups
+               if( $removegroup ) {
+                       $newGroups = array_diff($newGroups, $removegroup);
+                       foreach( $removegroup as $group ) {
+                               $user->removeGroup( $group );
+                       }
+               }
+               if( $addgroup ) {
+                       $newGroups = array_merge($newGroups, $addgroup);
+                       foreach( $addgroup as $group ) {
+                               $user->addGroup( $group );
+                       }
+               }
+               $newGroups = array_unique( $newGroups );
+
+               // Ensure that caches are cleared
+               $user->invalidateCache();
+
+               wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
+               wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
+               if( $user instanceof User ) {
+                       // hmmm
+                       wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
+               }
+
+               if( $newGroups != $oldGroups ) {
+                       $this->addLogEntry( $user, $oldGroups, $newGroups );
+               }
+       }
+       
+       /**
+        * Add a rights log entry for an action.
+        */
+       function addLogEntry( $user, $oldGroups, $newGroups ) {
+               global $wgRequest;
+               $log = new LogPage( 'rights' );
+
+               $log->addEntry( 'rights',
+                       $user->getUserPage(),
+                       $wgRequest->getText( 'user-reason' ),
+                       array(
+                               $this->makeGroupNameList( $oldGroups ),
+                               $this->makeGroupNameList( $newGroups )
+                       )
+               );
+       }
+
+       /**
+        * Edit user groups membership
+        * @param $username String: name of the user.
+        */
+       function editUserGroupsForm( $username ) {
+               global $wgOut;
+
+               $user = $this->fetchUser( $username );
+               if( !$user ) {
+                       return;
+               }
+
+               $groups = $user->getGroups();
+
+               $this->showEditUserGroupsForm( $user, $groups );
+
+               // This isn't really ideal logging behavior, but let's not hide the
+               // interwiki logs if we're using them as is.
+               $this->showLogFragment( $user, $wgOut );
+       }
+
+       /**
+        * Normalize the input username, which may be local or remote, and
+        * return a user (or proxy) object for manipulating it.
+        *
+        * Side effects: error output for invalid access
+        * @return mixed User, UserRightsProxy, or null
+        */
+       function fetchUser( $username ) {
+               global $wgOut, $wgUser;
+
+               $parts = explode( '@', $username );
+               if( count( $parts ) < 2 ) {
+                       $name = trim( $username );
+                       $database = '';
+               } else {
+                       list( $name, $database ) = array_map( 'trim', $parts );
+
+                       if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) {
+                               $wgOut->addWikiMsg( 'userrights-no-interwiki' );
+                               return null;
+                       }
+                       if( !UserRightsProxy::validDatabase( $database ) ) {
+                               $wgOut->addWikiMsg( 'userrights-nodatabase', $database );
+                               return null;
+                       }
+               }
+
+               if( $name == '' ) {
+                       $wgOut->addWikiMsg( 'nouserspecified' );
+                       return false;
+               }
+
+               if( $name{0} == '#' ) {
+                       // Numeric ID can be specified...
+                       // We'll do a lookup for the name internally.
+                       $id = intval( substr( $name, 1 ) );
+
+                       if( $database == '' ) {
+                               $name = User::whoIs( $id );
+                       } else {
+                               $name = UserRightsProxy::whoIs( $database, $id );
+                       }
+
+                       if( !$name ) {
+                               $wgOut->addWikiMsg( 'noname' );
+                               return null;
+                       }
+               }
+
+               if( $database == '' ) {
+                       $user = User::newFromName( $name );
+               } else {
+                       $user = UserRightsProxy::newFromName( $database, $name );
+               }
+
+               if( !$user || $user->isAnon() ) {
+                       $wgOut->addWikiMsg( 'nosuchusershort', $username );
+                       return null;
+               }
+
+               return $user;
+       }
+
+       function makeGroupNameList( $ids ) {
+               return implode( ', ', $ids );
+       }
+
+       /**
+        * Output a form to allow searching for a user
+        */
+       function switchForm() {
+               global $wgOut, $wgScript;
+               $wgOut->addHTML(
+                       Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
+                       Xml::hidden( 'title',  $this->getTitle()->getPrefixedText() ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', array(), wfMsg( 'userrights-lookup-user' ) ) .
+                       Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, $this->mTarget ) . ' ' .
+                       Xml::submitButton( wfMsg( 'editusergroup' ) ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+       }
+
+       /**
+        * Go through used and available groups and return the ones that this
+        * form will be able to manipulate based on the current user's system
+        * permissions.
+        *
+        * @param $groups Array: list of groups the given user is in
+        * @return Array:  Tuple of addable, then removable groups
+        */
+       protected function splitGroups( $groups ) {
+               global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+               list($addable, $removable) = array_values( $this->changeableGroups() );
+
+               $removable = array_intersect(
+                               array_merge($this->isself ? $wgGroupsRemoveFromSelf : array(), $removable),
+                               $groups ); // Can't remove groups the user doesn't have
+               $addable   = array_diff(
+                               array_merge($this->isself ? $wgGroupsAddToSelf : array(), $addable),
+                               $groups ); // Can't add groups the user does have
+
+               return array( $addable, $removable );
+       }
+
+       /**
+        * Show the form to edit group memberships.
+        *
+        * @param $user      User or UserRightsProxy you're editing
+        * @param $groups    Array:  Array of groups the user is in
+        */
+       protected function showEditUserGroupsForm( $user, $groups ) {
+               global $wgOut, $wgUser;
+
+               list( $addable, $removable ) = $this->splitGroups( $groups );
+
+               $list = array();
+               foreach( $user->getGroups() as $group )
+                       $list[] = self::buildGroupLink( $group );
+
+               $grouplist = '';
+               if( count( $list ) > 0 ) {
+                       $grouplist = Xml::tags( 'p', null, wfMsgHtml( 'userrights-groupsmember' ) . ' ' . implode( ', ', $list ) );
+               }
+               $wgOut->addHTML(
+                       Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
+                       Xml::hidden( 'user', $this->mTarget ) .
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->mTarget ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
+                       wfMsgExt( 'editinguser', array( 'parse' ), wfEscapeWikiText( $user->getName() ) ) .
+                       wfMsgExt( 'userrights-groups-help', array( 'parse' ) ) .
+                       $grouplist .
+                       Xml::tags( 'p', null, $this->groupCheckboxes( $groups ) ) .
+                       Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) .
+                               "<tr>
+                                       <td class='mw-label'>" .
+                                               Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
+                                       "</td>
+                                       <td class='mw-input'>" .
+                                               Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
+                                       "</td>
+                               </tr>
+                               <tr>
+                                       <td></td>
+                                       <td class='mw-submit'>" .
+                                               Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
+                                       "</td>
+                               </tr>" .
+                       Xml::closeElement( 'table' ) . "\n" .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' ) . "\n"
+               );
+       }
+
+       /**
+        * Format a link to a group description page
+        *
+        * @param $group string
+        * @return string
+        */
+       private static function buildGroupLink( $group ) {
+               static $cache = array();
+               if( !isset( $cache[$group] ) )
+                       $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupName( $group ) );
+               return $cache[$group];
+       }
+       
+       /**
+        * Returns an array of all groups that may be edited
+        * @return array Array of groups that may be edited.
+        */
+        protected static function getAllGroups() {
+               return User::getAllGroups();
+        }
+
+       /**
+        * Adds a table with checkboxes where you can select what groups to add/remove
+        *
+        * @param $usergroups Array: groups the user belongs to
+        * @return string XHTML table element with checkboxes
+        */
+       private function groupCheckboxes( $usergroups ) {
+               $allgroups = $this->getAllGroups();
+               $ret = '';
+
+               $column = 1;
+               $settable_col = '';
+               $unsettable_col = '';
+
+               foreach ($allgroups as $group) {
+                       $set = in_array( $group, $usergroups );
+                       # Should the checkbox be disabled?
+                       $disabled = !(
+                               ( $set && $this->canRemove( $group ) ) ||
+                               ( !$set && $this->canAdd( $group ) ) );
+                       # Do we need to point out that this action is irreversible?
+                       $irreversible = !$disabled && (
+                               ($set && !$this->canAdd( $group )) ||
+                               (!$set && !$this->canRemove( $group ) ) );
+
+                       $attr = $disabled ? array( 'disabled' => 'disabled' ) : array();
+                       $text = $irreversible
+                               ? wfMsgHtml( 'userrights-irreversible-marker', User::getGroupMember( $group ) )
+                               : User::getGroupMember( $group );
+                       $checkbox = Xml::checkLabel( $text, "wpGroup-$group",
+                               "wpGroup-$group", $set, $attr );
+                       $checkbox = $disabled ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkbox ) : $checkbox;
+
+                       if ($disabled) {
+                               $unsettable_col .= "$checkbox<br />\n";
+                       } else {
+                               $settable_col .= "$checkbox<br />\n";
+                       }
+               }
+
+               if ($column) {
+                       $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) .
+                               "<tr>
+";
+                       if( $settable_col !== '' ) {
+                               $ret .= xml::element( 'th', null, wfMsg( 'userrights-changeable-col' ) );
+                       }
+                       if( $unsettable_col !== '' ) {
+                               $ret .= xml::element( 'th', null, wfMsg( 'userrights-unchangeable-col' ) );
+                       }
+                       $ret.= "</tr>
+                               <tr>
+";
+                       if( $settable_col !== '' ) {
+                               $ret .=
+"                                      <td style='vertical-align:top;'>
+                                               $settable_col
+                                       </td>
+";
+                       }
+                       if( $unsettable_col !== '' ) {
+                               $ret .=
+"                                      <td style='vertical-align:top;'>
+                                               $unsettable_col
+                                       </td>
+";
+                       }
+                       $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * @param  $group String: the name of the group to check
+        * @return bool Can we remove the group?
+        */
+       private function canRemove( $group ) {
+               // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
+               // PHP.
+               $groups = $this->changeableGroups();
+               return in_array( $group, $groups['remove'] ) || ($this->isself && in_array( $group, $groups['remove-self'] ));
+       }
+
+       /**
+        * @param $group string: the name of the group to check
+        * @return bool Can we add the group?
+        */
+       private function canAdd( $group ) {
+               $groups = $this->changeableGroups();
+               return in_array( $group, $groups['add'] ) || ($this->isself && in_array( $group, $groups['add-self'] ));
+       }
+
+       /**
+        * Returns an array of the groups that the user can add/remove.
+        *
+        * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+        */
+       function changeableGroups() {
+               global $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+               if( $wgUser->isAllowed( 'userrights' ) ) {
+                       // This group gives the right to modify everything (reverse-
+                       // compatibility with old "userrights lets you change
+                       // everything")
+                       // Using array_merge to make the groups reindexed
+                       $all = array_merge( User::getAllGroups() );
+                       return array(
+                               'add' => $all,
+                               'remove' => $all,
+                               'add-self' => array(),
+                               'remove-self' => array()
+                       );
+               }
+
+               // Okay, it's not so simple, we will have to go through the arrays
+               $groups = array(
+                               'add' => array(),
+                               'remove' => array(),
+                               'add-self' => $wgGroupsAddToSelf,
+                               'remove-self' => $wgGroupsRemoveFromSelf);
+               $addergroups = $wgUser->getEffectiveGroups();
+
+               foreach ($addergroups as $addergroup) {
+                       $groups = array_merge_recursive(
+                               $groups, $this->changeableByGroup($addergroup)
+                       );
+                       $groups['add']    = array_unique( $groups['add'] );
+                       $groups['remove'] = array_unique( $groups['remove'] );
+               }
+               return $groups;
+       }
+
+       /**
+        * Returns an array of the groups that a particular group can add/remove.
+        *
+        * @param $group String: the group to check for whether it can add/remove
+        * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
+        */
+       private function changeableByGroup( $group ) {
+               global $wgAddGroups, $wgRemoveGroups;
+
+               $groups = array( 'add' => array(), 'remove' => array() );
+               if( empty($wgAddGroups[$group]) ) {
+                       // Don't add anything to $groups
+               } elseif( $wgAddGroups[$group] === true ) {
+                       // You get everything
+                       $groups['add'] = User::getAllGroups();
+               } elseif( is_array($wgAddGroups[$group]) ) {
+                       $groups['add'] = $wgAddGroups[$group];
+               }
+
+               // Same thing for remove
+               if( empty($wgRemoveGroups[$group]) ) {
+               } elseif($wgRemoveGroups[$group] === true ) {
+                       $groups['remove'] = User::getAllGroups();
+               } elseif( is_array($wgRemoveGroups[$group]) ) {
+                       $groups['remove'] = $wgRemoveGroups[$group];
+               }
+               return $groups;
+       }
+
+       /**
+        * Show a rights log fragment for the specified user
+        *
+        * @param $user User to show log for
+        * @param $output OutputPage to use
+        */
+       protected function showLogFragment( $user, $output ) {
+               $output->addHtml( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
+               LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage()->getPrefixedText() );
+       }
+}
diff --git a/includes/specials/Version.php b/includes/specials/Version.php
new file mode 100644 (file)
index 0000000..8771fa1
--- /dev/null
@@ -0,0 +1,390 @@
+<?php
+/**#@+
+ * Give information about the version of MediaWiki, PHP, the DB and extensions
+ *
+ * @file
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+
+/**
+ * constructor
+ */
+function wfSpecialVersion() {
+       $version = new SpecialVersion;
+       $version->execute();
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class SpecialVersion {
+       private $firstExtOpened = true;
+
+       /**
+        * main()
+        */
+       function execute() {
+               global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks;
+               $wgMessageCache->loadAllMessages();
+
+               $wgOut->addHTML( '<div dir="ltr">' );
+               $text = 
+                       $this->MediaWikiCredits() .
+                       $this->softwareInformation() .
+                       $this->extensionCredits();
+               if  ( $wgSpecialVersionShowHooks ) {
+                       $text .= $this->wgHooks();
+               }
+               $wgOut->addWikiText( $text );
+               $wgOut->addHTML( $this->IPInfo() );
+               $wgOut->addHTML( '</div>' );
+       }
+
+       /**#@+
+        * @private
+        */
+
+       /**
+        * @return wiki text showing the license information
+        */
+       static function MediaWikiCredits() {
+               $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
+               "__NOTOC__
+               This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
+               copyright (C) 2001-2008 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
+               Tim Starling, Erik Möller, Gabriel Wicke, Ã†var Arnfjörð Bjarmason,
+               Niklas Laxström, Domas Mituzas, Rob Church, Yuri Astrakhan and others.
+
+               MediaWiki is free software; you can redistribute it and/or modify
+               it under the terms of the GNU General Public License as published by
+               the Free Software Foundation; either version 2 of the License, or
+               (at your option) any later version.
+
+               MediaWiki is distributed in the hope that it will be useful,
+               but WITHOUT ANY WARRANTY; without even the implied warranty of
+               MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+               GNU General Public License for more details.
+
+               You should have received [{{SERVER}}{{SCRIPTPATH}}/COPYING a copy of the GNU General Public License]
+               along with this program; if not, write to the Free Software
+               Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+               or [http://www.gnu.org/licenses/old-licenses/gpl-2.0.html read it online].
+               ";
+
+               return str_replace( "\t\t", '', $ret ) . "\n";
+       }
+
+       /**
+        * @return wiki text showing the third party software versions (apache, php, mysql).
+        */
+       static function softwareInformation() {
+               $dbr = wfGetDB( DB_SLAVE );
+
+               return Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
+                       Xml::openElement( 'table', array( 'id' => 'sv-software' ) ) .
+                               "<tr>
+                                       <th>" . wfMsg( 'version-software-product' ) . "</th>
+                                       <th>" . wfMsg( 'version-software-version' ) . "</th>
+                               </tr>\n
+                               <tr>
+                                       <td>[http://www.mediawiki.org/ MediaWiki]</td>
+                                       <td>" . self::getVersionLinked() . "</td>
+                               </tr>\n
+                               <tr>
+                                       <td>[http://www.php.net/ PHP]</td>
+                                       <td>" . phpversion() . " (" . php_sapi_name() . ")</td>
+                               </tr>\n
+                               <tr>
+                                       <td>" . $dbr->getSoftwareLink() . "</td>
+                                       <td>" . $dbr->getServerVersion() . "</td>
+                               </tr>\n" .
+                       Xml::closeElement( 'table' );
+       }
+
+       /**
+        * Return a string of the MediaWiki version with SVN revision if available
+        *
+        * @return mixed
+        */
+       public static function getVersion() {
+               global $wgVersion, $IP;
+               wfProfileIn( __METHOD__ );
+               $svn = self::getSvnRevision( $IP );
+               $version = $svn ? "$wgVersion (r$svn)" : $wgVersion;
+               wfProfileOut( __METHOD__ );
+               return $version;
+       }
+       
+       /**
+        * Return a string of the MediaWiki version with a link to SVN revision if
+        * available
+        *
+        * @return mixed
+        */
+       public static function getVersionLinked() {
+               global $wgVersion, $IP;
+               wfProfileIn( __METHOD__ );
+               $svn = self::getSvnRevision( $IP );
+               $viewvc = 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/?pathrev=';
+               $version = $svn ? "$wgVersion ([{$viewvc}{$svn} r$svn])" : $wgVersion;
+               wfProfileOut( __METHOD__ );
+               return $version;
+       }
+
+       /** Generate wikitext showing extensions name, URL, author and description */
+       function extensionCredits() {
+               global $wgExtensionCredits, $wgExtensionFunctions, $wgParser, $wgSkinExtensionFunctions;
+
+               if ( ! count( $wgExtensionCredits ) && ! count( $wgExtensionFunctions ) && ! count( $wgSkinExtensionFunctions ) )
+                       return '';
+
+               $extensionTypes = array(
+                       'specialpage' => wfMsg( 'version-specialpages' ),
+                       'parserhook' => wfMsg( 'version-parserhooks' ),
+                       'variable' => wfMsg( 'version-variables' ),
+                       'media' => wfMsg( 'version-mediahandlers' ),
+                       'other' => wfMsg( 'version-other' ),
+               );
+               wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
+
+               $out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
+                       Xml::openElement( 'table', array( 'id' => 'sv-ext' ) );
+
+               foreach ( $extensionTypes as $type => $text ) {
+                       if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
+                               $out .= $this->openExtType( $text );
+
+                               usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
+
+                               foreach ( $wgExtensionCredits[$type] as $extension ) {
+                                       if ( isset( $extension['version'] ) ) {
+                                               $version = $extension['version'];
+                                       } elseif ( isset( $extension['svn-revision'] ) && 
+                                               preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/', 
+                                                       $extension['svn-revision'], $m ) ) 
+                                       {
+                                               $version = 'r' . $m[1];
+                                       } else {
+                                               $version = null;
+                                       }
+
+                                       $out .= $this->formatCredits(
+                                               isset ( $extension['name'] )           ? $extension['name']        : '',
+                                               $version,
+                                               isset ( $extension['author'] )         ? $extension['author']      : '',
+                                               isset ( $extension['url'] )            ? $extension['url']         : null,
+                                               isset ( $extension['description'] )    ? $extension['description'] : '',
+                                               isset ( $extension['descriptionmsg'] ) ? $extension['descriptionmsg'] : ''
+                                       );
+                               }
+                       }
+               }
+
+               if ( count( $wgExtensionFunctions ) ) {
+                       $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) );
+                       $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
+               }
+
+               if ( $cnt = count( $tags = $wgParser->getTags() ) ) {
+                       for ( $i = 0; $i < $cnt; ++$i )
+                               $tags[$i] = "&lt;{$tags[$i]}&gt;";
+                       $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) );
+                       $out .= '<tr><td colspan="3">' . $this->listToText( $tags ). "</td></tr>\n";
+               }
+
+               if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) {
+                       $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) );
+                       $out .= '<tr><td colspan="3">' . $this->listToText( $fhooks ) . "</td></tr>\n";
+               }
+
+               if ( count( $wgSkinExtensionFunctions ) ) {
+                       $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) );
+                       $out .= '<tr><td colspan="3">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
+               }
+               $out .= Xml::closeElement( 'table' );
+               return $out;
+       }
+
+       /** Callback to sort extensions by type */
+       function compare( $a, $b ) {
+               global $wgLang;
+               if( $a['name'] === $b['name'] ) {
+                       return 0;
+               } else {
+                       return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] )
+                               ? 1
+                               : -1;
+               }
+       }
+
+       function formatCredits( $name, $version = null, $author = null, $url = null, $description = null, $descriptionMsg = null ) {
+               $extension = isset( $url ) ? "[$url $name]" : $name;
+               $version = isset( $version ) ? "(" . wfMsg( 'version-version' ) . " $version)" : '';
+
+               # Look for a localized description
+               if( isset( $descriptionMsg ) ) {
+                       $msg = wfMsg( $descriptionMsg );
+                       if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
+                               $description = $msg;
+                       }
+               }
+
+               return "<tr>
+                               <td><em>$extension $version</em></td>
+                               <td>$description</td>
+                               <td>" . $this->listToText( (array)$author ) . "</td>
+                       </tr>\n";
+       }
+
+       /**
+        * @return string
+        */
+       function wgHooks() {
+               global $wgHooks;
+
+               if ( count( $wgHooks ) ) {
+                       $myWgHooks = $wgHooks;
+                       ksort( $myWgHooks );
+
+                       $ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
+                               Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) .
+                               "<tr>
+                                       <th>" . wfMsg( 'version-hook-name' ) . "</th>
+                                       <th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
+                               </tr>\n";
+
+                       foreach ( $myWgHooks as $hook => $hooks )
+                               $ret .= "<tr>
+                                               <td>$hook</td>
+                                               <td>" . $this->listToText( $hooks ) . "</td>
+                                       </tr>\n";
+
+                       $ret .= Xml::closeElement( 'table' );
+                       return $ret;
+               } else
+                       return '';
+       }
+
+       private function openExtType($text, $name = null) {
+               $opt = array( 'colspan' => 3 );
+               $out = '';
+
+               if(!$this->firstExtOpened) {
+                       // Insert a spacing line
+                       $out .= '<tr class="sv-space">' . Xml::element( 'td', $opt ) . "</tr>\n";
+               }
+               $this->firstExtOpened = false;
+
+               if($name) { $opt['id'] = "sv-$name"; }
+
+               $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n";
+               return $out;
+       }
+
+       /**
+        * @static
+        *
+        * @return string
+        */
+       function IPInfo() {
+               $ip =  str_replace( '--', ' - ', htmlspecialchars( wfGetIP() ) );
+               return "<!-- visited from $ip -->\n" .
+                       "<span style='display:none'>visited from $ip</span>";
+       }
+
+       /**
+        * @param array $list
+        * @return string
+        */
+       function listToText( $list ) {
+               $cnt = count( $list );
+
+               if ( $cnt == 1 ) {
+                       // Enforce always returning a string
+                       return (string)$this->arrayToString( $list[0] );
+               } elseif ( $cnt == 0 ) {
+                       return '';
+               } else {
+                       sort( $list );
+                       $t = array_slice( $list, 0, $cnt - 1 );
+                       $one = array_map( array( &$this, 'arrayToString' ), $t );
+                       $two = $this->arrayToString( $list[$cnt - 1] );
+                       $and = wfMsg( 'and' );
+
+                       return implode( ', ', $one ) . " $and $two";
+               }
+       }
+
+       /**
+        * @static
+        *
+        * @param mixed $list Will convert an array to string if given and return
+        *                    the paramater unaltered otherwise
+        * @return mixed
+        */
+       function arrayToString( $list ) {
+               if( is_object( $list ) ) {
+                       $class = get_class( $list );
+                       return "($class)";
+               } elseif ( ! is_array( $list ) ) {
+                       return $list;
+               } else {
+                       $class = get_class( $list[0] );
+                       return "($class, {$list[1]})";
+               }
+       }
+
+       /**
+        * Retrieve the revision number of a Subversion working directory.
+        *
+        * @param string $dir
+        * @return mixed revision number as int, or false if not a SVN checkout
+        */
+       public static function getSvnRevision( $dir ) {
+               // http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
+               $entries = $dir . '/.svn/entries';
+
+               if( !file_exists( $entries ) ) {
+                       return false;
+               }
+
+               $content = file( $entries );
+
+               // check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
+               if( preg_match( '/^<\?xml/', $content[0] ) ) {
+                       // subversion is release <= 1.3
+                       if( !function_exists( 'simplexml_load_file' ) ) {
+                               // We could fall back to expat... YUCK
+                               return false;
+                       }
+
+                       // SimpleXml whines about the xmlns...
+                       wfSuppressWarnings();
+                       $xml = simplexml_load_file( $entries );
+                       wfRestoreWarnings();
+
+                       if( $xml ) {
+                               foreach( $xml->entry as $entry ) {
+                                       if( $xml->entry[0]['name'] == '' ) {
+                                               // The directory entry should always have a revision marker.
+                                               if( $entry['revision'] ) {
+                                                       return intval( $entry['revision'] );
+                                               }
+                                       }
+                               }
+                       }
+                       return false;
+               } else {
+                       // subversion is release 1.4
+                       return intval( $content[3] );
+               }
+       }
+
+       /**#@-*/
+}
+
+/**#@-*/
diff --git a/includes/specials/Wantedcategories.php b/includes/specials/Wantedcategories.php
new file mode 100644 (file)
index 0000000..969a8d8
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * A querypage to list the most wanted categories - implements Special:Wantedcategories
+ *
+ * @ingroup SpecialPage
+ *
+ * @author Ã†var Arnfjörð Bjarmason <avarab@gmail.com>
+ * @copyright Copyright Â© 2005, Ã†var Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ */
+class WantedCategoriesPage extends QueryPage {
+
+       function getName() {
+               return 'Wantedcategories';
+       }
+
+       function isExpensive() {
+               return true;
+       }
+
+       function isSyndicated() {
+               return false;
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $categorylinks, $page ) = $dbr->tableNamesN( 'categorylinks', 'page' );
+               $name = $dbr->addQuotes( $this->getName() );
+               return
+                       "
+                       SELECT
+                               $name as type,
+                               " . NS_CATEGORY . " as namespace,
+                               cl_to as title,
+                               COUNT(*) as value
+                       FROM $categorylinks
+                       LEFT JOIN $page ON cl_to = page_title AND page_namespace = ". NS_CATEGORY ."
+                       WHERE page_title IS NULL
+                       GROUP BY 1,2,3
+                       ";
+       }
+
+       function sortDescending() { return true; }
+
+       /**
+        * Fetch user page links and cache their existence
+        */
+       function preprocessResults( $db, $res ) {
+               $batch = new LinkBatch;
+               while ( $row = $db->fetchObject( $res ) )
+                       $batch->add( $row->namespace, $row->title );
+               $batch->execute();
+
+               // Back to start for display
+               if ( $db->numRows( $res ) > 0 )
+                       // If there are no rows we get an error seeking.
+                       $db->dataSeek( $res, 0 );
+       }
+
+       function formatResult( $skin, $result ) {
+               global $wgLang, $wgContLang;
+
+               $nt = Title::makeTitle( $result->namespace, $result->title );
+               $text = $wgContLang->convert( $nt->getText() );
+
+               $plink = $this->isCached() ?
+                       $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
+                       $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
+
+               $nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
+                       $wgLang->formatNum( $result->value ) );
+               return wfSpecialList($plink, $nlinks);
+       }
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedCategories() {
+       list( $limit, $offset ) = wfCheckLimits();
+
+       $wpp = new WantedCategoriesPage();
+
+       $wpp->doQuery( $offset, $limit );
+}
diff --git a/includes/specials/Wantedpages.php b/includes/specials/Wantedpages.php
new file mode 100644 (file)
index 0000000..650e04f
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * implements Special:Wantedpages
+ * @ingroup SpecialPage
+ */
+class WantedPagesPage extends QueryPage {
+       var $nlinks;
+
+       function WantedPagesPage( $inc = false, $nlinks = true ) {
+               $this->setListoutput( $inc );
+               $this->nlinks = $nlinks;
+       }
+
+       function getName() {
+               return 'Wantedpages';
+       }
+
+       function isExpensive() {
+               return true;
+       }
+       function isSyndicated() { return false; }
+
+       function getSQL() {
+               global $wgWantedPagesThreshold;
+               $count = $wgWantedPagesThreshold - 1;
+               $dbr = wfGetDB( DB_SLAVE );
+               $pagelinks = $dbr->tableName( 'pagelinks' );
+               $page      = $dbr->tableName( 'page' );
+               return
+                       "SELECT 'Wantedpages' AS type,
+                               pl_namespace AS namespace,
+                               pl_title AS title,
+                               COUNT(*) AS value
+                        FROM $pagelinks
+                        LEFT JOIN $page AS pg1
+                        ON pl_namespace = pg1.page_namespace AND pl_title = pg1.page_title
+                        LEFT JOIN $page AS pg2
+                        ON pl_from = pg2.page_id
+                        WHERE pg1.page_namespace IS NULL
+                        AND pl_namespace NOT IN ( 2, 3 )
+                        AND pg2.page_namespace != 8
+                        GROUP BY 1,2,3
+                        HAVING COUNT(*) > $count";
+       }
+
+       /**
+        * Cache page existence for performance
+        */
+       function preprocessResults( $db, $res ) {
+               $batch = new LinkBatch;
+               while ( $row = $db->fetchObject( $res ) )
+                       $batch->add( $row->namespace, $row->title );
+               $batch->execute();
+
+               // Back to start for display
+               if ( $db->numRows( $res ) > 0 )
+                       // If there are no rows we get an error seeking.
+                       $db->dataSeek( $res, 0 );
+       }
+
+       /**
+        * Format an individual result
+        *
+        * @param $skin Skin to use for UI elements
+        * @param $result Result row
+        * @return string
+        */
+       public function formatResult( $skin, $result ) {
+               $title = Title::makeTitleSafe( $result->namespace, $result->title );
+               if( $title instanceof Title ) {
+                       if( $this->isCached() ) {
+                               $pageLink = $title->exists()
+                                       ? '<s>' . $skin->makeLinkObj( $title ) . '</s>'
+                                       : $skin->makeBrokenLinkObj( $title );
+                       } else {
+                               $pageLink = $skin->makeBrokenLinkObj( $title );
+                       }
+                       return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) );
+               } else {
+                       $tsafe = htmlspecialchars( $result->title );
+                       return "Invalid title in result set; {$tsafe}";
+               }
+       }
+
+       /**
+        * Make a "what links here" link for a specified result if required
+        *
+        * @param $title Title to make the link for
+        * @param $skin Skin to use
+        * @param $result Result row
+        * @return string
+        */
+       private function makeWlhLink( $title, $skin, $result ) {
+               global $wgLang;
+               if( $this->nlinks ) {
+                       $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+                       $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+                               $wgLang->formatNum( $result->value ) );
+                       return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() );
+               } else {
+                       return null;
+               }
+       }
+
+}
+
+/**
+ * constructor
+ */
+function wfSpecialWantedpages( $par = null, $specialPage ) {
+       $inc = $specialPage->including();
+
+       if ( $inc ) {
+               @list( $limit, $nlinks ) = explode( '/', $par, 2 );
+               $limit = (int)$limit;
+               $nlinks = $nlinks === 'nlinks';
+               $offset = 0;
+       } else {
+               list( $limit, $offset ) = wfCheckLimits();
+               $nlinks = true;
+       }
+
+       $wpp = new WantedPagesPage( $inc, $nlinks );
+
+       $wpp->doQuery( $offset, $limit, !$inc );
+}
diff --git a/includes/specials/Watchlist.php b/includes/specials/Watchlist.php
new file mode 100644 (file)
index 0000000..bb5e7ba
--- /dev/null
@@ -0,0 +1,377 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage Watchlist
+ */
+
+/**
+ *
+ */
+require_once( dirname(__FILE__) . '/SpecialRecentchanges.php' );
+
+/**
+ * Constructor
+ *
+ * @param $par Parameter passed to the page
+ */
+function wfSpecialWatchlist( $par ) {
+       global $wgUser, $wgOut, $wgLang, $wgRequest;
+       global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
+       global $wgEnotifWatchlist;
+       $fname = 'wfSpecialWatchlist';
+
+       $skin = $wgUser->getSkin();
+       $specialTitle = SpecialPage::getTitleFor( 'Watchlist' );
+       $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+       # Anons don't get a watchlist
+       if( $wgUser->isAnon() ) {
+               $wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
+               $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
+               $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
+               return;
+       }
+
+       $wgOut->setPageTitle( wfMsg( 'watchlist' ) );
+
+       $sub  = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() );
+       $sub .= '<br />' . WatchlistEditor::buildTools( $wgUser->getSkin() );
+       $wgOut->setSubtitle( $sub );
+
+       if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) {
+               $editor = new WatchlistEditor();
+               $editor->execute( $wgUser, $wgOut, $wgRequest, $mode );
+               return;
+       }
+
+       $uid = $wgUser->getId();
+       if( ($wgEnotifWatchlist || $wgShowUpdatedMarker) && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) {
+               $wgUser->clearAllNotifications( $uid );
+               $wgOut->redirect( $specialTitle->getFullUrl() );
+               return;
+       }
+
+       $defaults = array(
+       /* float */ 'days' => floatval( $wgUser->getOption( 'watchlistdays' ) ), /* 3.0 or 0.5, watch further below */
+       /* bool  */ 'hideOwn' => (int)$wgUser->getBoolOption( 'watchlisthideown' ),
+       /* bool  */ 'hideBots' => (int)$wgUser->getBoolOption( 'watchlisthidebots' ),
+       /* bool */ 'hideMinor' => (int)$wgUser->getBoolOption( 'watchlisthideminor' ),
+       /* ?     */ 'namespace' => 'all',
+       );
+
+       extract($defaults);
+
+       # Extract variables from the request, falling back to user preferences or
+       # other default values if these don't exist
+       $prefs['days'    ] = floatval( $wgUser->getOption( 'watchlistdays' ) );
+       $prefs['hideown' ] = $wgUser->getBoolOption( 'watchlisthideown' );
+       $prefs['hidebots'] = $wgUser->getBoolOption( 'watchlisthidebots' );
+       $prefs['hideminor'] = $wgUser->getBoolOption( 'watchlisthideminor' );
+
+       # Get query variables
+       $days     = $wgRequest->getVal(  'days', $prefs['days'] );
+       $hideOwn  = $wgRequest->getBool( 'hideOwn', $prefs['hideown'] );
+       $hideBots = $wgRequest->getBool( 'hideBots', $prefs['hidebots'] );
+       $hideMinor = $wgRequest->getBool( 'hideMinor', $prefs['hideminor'] );
+
+       # Get namespace value, if supplied, and prepare a WHERE fragment
+       $nameSpace = $wgRequest->getIntOrNull( 'namespace' );
+       if( !is_null( $nameSpace ) ) {
+               $nameSpace = intval( $nameSpace );
+               $nameSpaceClause = " AND rc_namespace = $nameSpace";
+       } else {
+               $nameSpace = '';
+               $nameSpaceClause = '';
+       }
+
+       $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+       list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' );
+
+       $watchlistCount = $dbr->selectField( 'watchlist', 'COUNT(*)',
+               array( 'wl_user' => $uid ), __METHOD__ );
+       // Adjust for page X, talk:page X, which are both stored separately,
+       // but treated together
+       $nitems = floor($watchlistCount / 2);
+
+       if( is_null($days) || !is_numeric($days) ) {
+               $big = 1000; /* The magical big */
+               if($nitems > $big) {
+                       # Set default cutoff shorter
+                       $days = $defaults['days'] = (12.0 / 24.0); # 12 hours...
+               } else {
+                       $days = $defaults['days']; # default cutoff for shortlisters
+               }
+       } else {
+               $days = floatval($days);
+       }
+
+       // Dump everything here
+       $nondefaults = array();
+
+       wfAppendToArrayIfNotDefault('days'     , $days         , $defaults, $nondefaults);
+       wfAppendToArrayIfNotDefault('hideOwn'  , (int)$hideOwn , $defaults, $nondefaults);
+       wfAppendToArrayIfNotDefault('hideBots' , (int)$hideBots, $defaults, $nondefaults);
+       wfAppendToArrayIfNotDefault( 'hideMinor', (int)$hideMinor, $defaults, $nondefaults );
+       wfAppendToArrayIfNotDefault('namespace', $nameSpace    , $defaults, $nondefaults);
+
+       $hookSql = "";
+       if( ! wfRunHooks('BeforeWatchlist', array($nondefaults, $wgUser, &$hookSql)) ) {
+               return;
+       }
+
+       if($nitems == 0) {
+               $wgOut->addWikiMsg( 'nowatchlist' );
+               return;
+       }
+
+       if ( $days <= 0 ) {
+               $andcutoff = '';
+       } else {
+               $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'";
+               /*
+               $sql = "SELECT COUNT(*) AS n FROM $page, $revision  WHERE rev_timestamp>'$cutoff' AND page_id=rev_page";
+               $res = $dbr->query( $sql, $fname );
+               $s = $dbr->fetchObject( $res );
+               $npages = $s->n;
+               */
+       }
+
+       # If the watchlist is relatively short, it's simplest to zip
+       # down its entirety and then sort the results.
+
+       # If it's relatively long, it may be worth our while to zip
+       # through the time-sorted page list checking for watched items.
+
+       # Up estimate of watched items by 15% to compensate for talk pages...
+
+       # Toggles
+       $andHideOwn = $hideOwn ? "AND (rc_user <> $uid)" : '';
+       $andHideBots = $hideBots ? "AND (rc_bot = 0)" : '';
+       $andHideMinor = $hideMinor ? 'AND rc_minor = 0' : '';
+
+       # Show watchlist header
+       $header = '';
+       if( $wgUser->getOption( 'enotifwatchlistpages' ) && $wgEnotifWatchlist) {
+               $header .= wfMsg( 'wlheader-enotif' ) . "\n";
+       }
+       if ( $wgShowUpdatedMarker ) {
+               $header .= wfMsg( 'wlheader-showupdated' ) . "\n";
+       }
+
+  # Toggle watchlist content (all recent edits or just the latest)
+       if( $wgUser->getOption( 'extendwatchlist' )) {
+               $andLatest='';
+               $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
+       } else {
+       # Top log Ids for a page are not stored
+               $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
+               $limitWatchlist = '';
+       }
+
+       $header .= wfMsgExt( 'watchlist-details', array( 'parsemag' ), $wgLang->formatNum( $nitems ) );
+       $wgOut->addWikiText( $header );
+
+       # Show a message about slave lag, if applicable
+       if( ( $lag = $dbr->getLag() ) > 0 )
+               $wgOut->showLagWarning( $lag );
+
+       if ( $wgShowUpdatedMarker ) {
+               $wgOut->addHTML( '<form action="' .
+                       $specialTitle->escapeLocalUrl() .
+                       '" method="post"><input type="submit" name="dummy" value="' .
+                       htmlspecialchars( wfMsg( 'enotif_reset' ) ) .
+                       '" /><input type="hidden" name="reset" value="all" /></form>' .
+                       "\n\n" );
+       }
+       if ( $wgShowUpdatedMarker ) {
+               $wltsfield = ", ${watchlist}.wl_notificationtimestamp ";
+       } else {
+               $wltsfield = '';
+       }
+       $sql = "SELECT ${recentchanges}.* ${wltsfield}
+         FROM $watchlist,$recentchanges
+         LEFT JOIN $page ON rc_cur_id=page_id
+         WHERE wl_user=$uid
+         AND wl_namespace=rc_namespace
+         AND wl_title=rc_title
+         $andcutoff
+         $andLatest
+         $andHideOwn
+         $andHideBots
+         $andHideMinor
+         $nameSpaceClause
+         $hookSql
+         ORDER BY rc_timestamp DESC
+         $limitWatchlist";
+
+       $res = $dbr->query( $sql, $fname );
+       $numRows = $dbr->numRows( $res );
+
+       /* Start bottom header */
+       $wgOut->addHTML( "<hr />\n" );
+
+       if($days >= 1) {
+               $wgOut->addWikiText( wfMsgExt( 'rcnote', array( 'parseinline' ), $wgLang->formatNum( $numRows ),
+                       $wgLang->formatNum( $days ), $wgLang->timeAndDate( wfTimestampNow(), true ) ) . '<br />' , false );
+       } elseif($days > 0) {
+               $wgOut->addWikiText( wfMsgExt( 'wlnote', array( 'parseinline' ), $wgLang->formatNum( $numRows ),
+                       $wgLang->formatNum( round($days*24) ) ) . '<br />' , false );
+       }
+
+       $wgOut->addHTML( "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n" );
+
+       # Spit out some control panel links
+       $thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
+       $skin = $wgUser->getSkin();
+
+       # Hide/show bot edits
+       $label = $hideBots ? wfMsgHtml( 'watchlist-show-bots' ) : wfMsgHtml( 'watchlist-hide-bots' );
+       $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
+       $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+       # Hide/show own edits
+       $label = $hideOwn ? wfMsgHtml( 'watchlist-show-own' ) : wfMsgHtml( 'watchlist-hide-own' );
+       $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
+       $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+       # Hide/show minor edits
+       $label = $hideMinor ? wfMsgHtml( 'watchlist-show-minor' ) : wfMsgHtml( 'watchlist-hide-minor' );
+       $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults );
+       $links[] = $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits );
+
+       $wgOut->addHTML( implode( ' | ', $links ) );
+
+       # Form for namespace filtering
+       $form  = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $thisTitle->getLocalUrl() ) );
+       $form .= '<p>';
+       $form .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;';
+       $form .= Xml::namespaceSelector( $nameSpace, '' ) . '&nbsp;';
+       $form .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '</p>';
+       $form .= Xml::hidden( 'days', $days );
+       if( $hideOwn )
+               $form .= Xml::hidden( 'hideOwn', 1 );
+       if( $hideBots )
+               $form .= Xml::hidden( 'hideBots', 1 );
+       if( $hideMinor )
+               $form .= Xml::hidden( 'hideMinor', 1 );
+       $form .= Xml::closeElement( 'form' );
+       $wgOut->addHtml( $form );
+
+       # If there's nothing to show, stop here
+       if( $numRows == 0 ) {
+               $wgOut->addWikiMsg( 'watchnochange' );
+               return;
+       }
+
+       /* End bottom header */
+
+       /* Do link batch query */
+       $linkBatch = new LinkBatch;
+       while ( $row = $dbr->fetchObject( $res ) ) {
+               $userNameUnderscored = str_replace( ' ', '_', $row->rc_user_text );
+               if ( $row->rc_user != 0 ) {
+                       $linkBatch->add( NS_USER, $userNameUnderscored );
+               }
+               $linkBatch->add( NS_USER_TALK, $userNameUnderscored );
+       }
+       $linkBatch->execute();
+       $dbr->dataSeek( $res, 0 );
+
+       $list = ChangesList::newFromUser( $wgUser );
+
+       $s = $list->beginRecentChangesList();
+       $counter = 1;
+       while ( $obj = $dbr->fetchObject( $res ) ) {
+               # Make RC entry
+               $rc = RecentChange::newFromRow( $obj );
+               $rc->counter = $counter++;
+
+               if ( $wgShowUpdatedMarker ) {
+                       $updated = $obj->wl_notificationtimestamp;
+               } else {
+                       $updated = false;
+               }
+
+               if ($wgRCShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' )) {
+                       $rc->numberofWatchingusers = $dbr->selectField( 'watchlist',
+                               'COUNT(*)',
+                               array(
+                                       'wl_namespace' => $obj->rc_namespace,
+                                       'wl_title' => $obj->rc_title,
+                               ),
+                               __METHOD__ );
+               } else {
+                       $rc->numberofWatchingusers = 0;
+               }
+
+               $s .= $list->recentChangesLine( $rc, $updated );
+       }
+       $s .= $list->endRecentChangesList();
+
+       $dbr->freeResult( $res );
+       $wgOut->addHTML( $s );
+
+}
+
+function wlHoursLink( $h, $page, $options = array() ) {
+       global $wgUser, $wgLang, $wgContLang;
+       $sk = $wgUser->getSkin();
+       $s = $sk->makeKnownLink(
+         $wgContLang->specialPage( $page ),
+         $wgLang->formatNum( $h ),
+         wfArrayToCGI( array('days' => ($h / 24.0)), $options ) );
+       return $s;
+}
+
+function wlDaysLink( $d, $page, $options = array() ) {
+       global $wgUser, $wgLang, $wgContLang;
+       $sk = $wgUser->getSkin();
+       $s = $sk->makeKnownLink(
+         $wgContLang->specialPage( $page ),
+         ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) ),
+         wfArrayToCGI( array('days' => $d), $options ) );
+       return $s;
+}
+
+/**
+ * Returns html
+ */
+function wlCutoffLinks( $days, $page = 'Watchlist', $options = array() ) {
+       $hours = array( 1, 2, 6, 12 );
+       $days = array( 1, 3, 7 );
+       $i = 0;
+       foreach( $hours as $h ) {
+               $hours[$i++] = wlHoursLink( $h, $page, $options );
+       }
+       $i = 0;
+       foreach( $days as $d ) {
+               $days[$i++] = wlDaysLink( $d, $page, $options );
+       }
+       return wfMsgExt('wlshowlast',
+               array('parseinline', 'replaceafter'),
+               implode(' | ', $hours),
+               implode(' | ', $days),
+               wlDaysLink( 0, $page, $options ) );
+}
+
+/**
+ * Count the number of items on a user's watchlist
+ *
+ * @param $talk Include talk pages
+ * @return integer
+ */
+function wlCountItems( &$user, $talk = true ) {
+       $dbr = wfGetDB( DB_SLAVE, 'watchlist' );
+
+       # Fetch the raw count
+       $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->mId ), 'wlCountItems' );
+       $row = $dbr->fetchObject( $res );
+       $count = $row->count;
+       $dbr->freeResult( $res );
+
+       # Halve to remove talk pages if needed
+       if( !$talk )
+               $count = floor( $count / 2 );
+
+       return( $count );
+}
diff --git a/includes/specials/Whatlinkshere.php b/includes/specials/Whatlinkshere.php
new file mode 100644 (file)
index 0000000..a57df5e
--- /dev/null
@@ -0,0 +1,408 @@
+<?php
+/**
+ * @todo Use some variant of Pager or something; the pagination here is lousy.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Entry point
+ * @param $par String: An article name ??
+ */
+function wfSpecialWhatlinkshere($par = NULL) {
+       global $wgRequest;
+       $page = new WhatLinksHerePage( $wgRequest, $par );
+       $page->execute();
+}
+
+/**
+ * implements Special:Whatlinkshere
+ * @ingroup SpecialPage
+ */
+class WhatLinksHerePage {
+       // Stored data
+       protected $par;
+
+       // Stored objects
+       protected $opts, $target, $selfTitle;
+
+       // Stored globals
+       protected $skin, $request;
+
+       protected $limits = array( 20, 50, 100, 250, 500 );
+
+       function WhatLinksHerePage( $request, $par = null ) {
+               global $wgUser;
+               $this->request = $request;
+               $this->skin = $wgUser->getSkin();
+               $this->par = $par;
+       }
+
+       function execute() {
+               global $wgOut;
+
+               $opts = new FormOptions();
+
+               $opts->add( 'target', '' );
+               $opts->add( 'namespace', '', FormOptions::INTNULL );
+               $opts->add( 'limit', 50 );
+               $opts->add( 'from', 0 );
+               $opts->add( 'back', 0 );
+               $opts->add( 'hideredirs', false );
+               $opts->add( 'hidetrans', false );
+               $opts->add( 'hidelinks', false );
+               $opts->add( 'hideimages', false );
+
+               $opts->fetchValuesFromRequest( $this->request );
+               $opts->validateIntBounds( 'limit', 0, 5000 );
+
+               // Give precedence to subpage syntax
+               if ( isset($this->par) ) {
+                       $opts->setValue( 'target', $this->par );
+               }
+
+               // Bind to member variable
+               $this->opts = $opts;
+
+               $this->target = Title::newFromURL( $opts->getValue( 'target' ) );
+               if( !$this->target ) {
+                       $wgOut->addHTML( $this->whatlinkshereForm() );
+                       return;
+               }
+
+               $this->selfTitle = SpecialPage::getTitleFor( 'Whatlinkshere', $this->target->getPrefixedDBkey() );
+
+               $wgOut->setPageTitle( wfMsgExt( 'whatlinkshere-title', 'escape', $this->target->getPrefixedText() ) );
+               $wgOut->setSubtitle( wfMsgHtml( 'linklistsub' ) );
+
+               $wgOut->addHTML( wfMsgExt( 'whatlinkshere-barrow', array( 'escapenoentities') ) . ' '  .$this->skin->makeLinkObj($this->target, '', 'redirect=no' )."<br />\n");
+
+               $this->showIndirectLinks( 0, $this->target, $opts->getValue( 'limit' ),
+                       $opts->getValue( 'from' ), $opts->getValue( 'back' ) );
+       }
+
+       /**
+        * @param $level  int     Recursion level
+        * @param $target Title   Target title
+        * @param $limit  int     Number of entries to display
+        * @param $from   Title   Display from this article ID
+        * @param $back   Title   Display from this article ID at backwards scrolling
+        * @private
+        */
+       function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
+               global $wgOut, $wgMaxRedirectLinksRetrieved;
+               $dbr = wfGetDB( DB_SLAVE );
+               $options = array();
+
+               $hidelinks = $this->opts->getValue( 'hidelinks' );
+               $hideredirs = $this->opts->getValue( 'hideredirs' );
+               $hidetrans = $this->opts->getValue( 'hidetrans' );
+               $hideimages = $target->getNamespace() != NS_IMAGE || $this->opts->getValue( 'hideimages' );
+
+               $fetchlinks = (!$hidelinks || !$hideredirs);
+
+               // Make the query
+               $plConds = array(
+                       'page_id=pl_from',
+                       'pl_namespace' => $target->getNamespace(),
+                       'pl_title' => $target->getDBkey(),
+               );
+               if( $hideredirs ) {
+                       $plConds['page_is_redirect'] = 0;
+               } elseif( $hidelinks ) {
+                       $plConds['page_is_redirect'] = 1;
+               }
+
+               $tlConds = array(
+                       'page_id=tl_from',
+                       'tl_namespace' => $target->getNamespace(),
+                       'tl_title' => $target->getDBkey(),
+               );
+
+               $ilConds = array(
+                       'page_id=il_from',
+                       'il_to' => $target->getDBkey(),
+               );
+
+               $namespace = $this->opts->getValue( 'namespace' );
+               if ( is_int($namespace) ) {
+                       $plConds['page_namespace'] = $namespace;
+                       $tlConds['page_namespace'] = $namespace;
+                       $ilConds['page_namespace'] = $namespace;
+               }
+
+               if ( $from ) {
+                       $tlConds[] = "tl_from >= $from";
+                       $plConds[] = "pl_from >= $from";
+                       $ilConds[] = "il_from >= $from";
+               }
+
+               // Read an extra row as an at-end check
+               $queryLimit = $limit + 1;
+
+               // Enforce join order, sometimes namespace selector may
+               // trigger filesorts which are far less efficient than scanning many entries
+               $options[] = 'STRAIGHT_JOIN';
+
+               $options['LIMIT'] = $queryLimit;
+               $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' );
+
+               if( $fetchlinks ) {
+                       $options['ORDER BY'] = 'pl_from';
+                       $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields,
+                               $plConds, __METHOD__, $options );
+               }
+
+               if( !$hidetrans ) {
+                       $options['ORDER BY'] = 'tl_from';
+                       $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields,
+                               $tlConds, __METHOD__, $options );
+               }
+
+               if( !$hideimages ) {
+                       $options['ORDER BY'] = 'il_from';
+                       $ilRes = $dbr->select( array( 'imagelinks', 'page' ), $fields,
+                               $ilConds, __METHOD__, $options );
+               }
+
+               if( ( !$fetchlinks || !$dbr->numRows($plRes) ) && ( $hidetrans || !$dbr->numRows($tlRes) ) && ( $hideimages || !$dbr->numRows($ilRes) ) ) {
+                       if ( 0 == $level ) {
+                               $wgOut->addHTML( $this->whatlinkshereForm() );
+                               $errMsg = is_int($namespace) ? 'nolinkshere-ns' : 'nolinkshere';
+                               $wgOut->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
+                               // Show filters only if there are links
+                               if( $hidelinks || $hidetrans || $hideredirs || $hideimages )
+                                       $wgOut->addHTML( $this->getFilterPanel() );
+                       }
+                       return;
+               }
+
+               // Read the rows into an array and remove duplicates
+               // templatelinks comes second so that the templatelinks row overwrites the
+               // pagelinks row, so we get (inclusion) rather than nothing
+               if( $fetchlinks ) {
+                       while ( $row = $dbr->fetchObject( $plRes ) ) {
+                               $row->is_template = 0;
+                               $row->is_image = 0;
+                               $rows[$row->page_id] = $row;
+                       }
+                       $dbr->freeResult( $plRes );
+
+               }
+               if( !$hidetrans ) {
+                       while ( $row = $dbr->fetchObject( $tlRes ) ) {
+                               $row->is_template = 1;
+                               $row->is_image = 0;
+                               $rows[$row->page_id] = $row;
+                       }
+                       $dbr->freeResult( $tlRes );
+               }
+               if( !$hideimages ) {
+                       while ( $row = $dbr->fetchObject( $ilRes ) ) {
+                               $row->is_template = 0;
+                               $row->is_image = 1;
+                               $rows[$row->page_id] = $row;
+                       }
+                       $dbr->freeResult( $ilRes );
+               }
+
+               // Sort by key and then change the keys to 0-based indices
+               ksort( $rows );
+               $rows = array_values( $rows );
+
+               $numRows = count( $rows );
+
+               // Work out the start and end IDs, for prev/next links
+               if ( $numRows > $limit ) {
+                       // More rows available after these ones
+                       // Get the ID from the last row in the result set
+                       $nextId = $rows[$limit]->page_id;
+                       // Remove undisplayed rows
+                       $rows = array_slice( $rows, 0, $limit );
+               } else {
+                       // No more rows after
+                       $nextId = false;
+               }
+               $prevId = $from;
+
+               if ( $level == 0 ) {
+                       $wgOut->addHTML( $this->whatlinkshereForm() );
+                       $wgOut->addHTML( $this->getFilterPanel() );
+                       $wgOut->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
+
+                       $prevnext = $this->getPrevNext( $prevId, $nextId );
+                       $wgOut->addHTML( $prevnext );
+               }
+
+               $wgOut->addHTML( $this->listStart() );
+               foreach ( $rows as $row ) {
+                       $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
+
+                       if ( $row->page_is_redirect && $level < 2 ) {
+                               $wgOut->addHTML( $this->listItem( $row, $nt, true ) );
+                               $this->showIndirectLinks( $level + 1, $nt, $wgMaxRedirectLinksRetrieved );
+                               $wgOut->addHTML( Xml::closeElement( 'li' ) );
+                       } else {
+                               $wgOut->addHTML( $this->listItem( $row, $nt ) );
+                       }
+               }
+
+               $wgOut->addHTML( $this->listEnd() );
+
+               if( $level == 0 ) {
+                       $wgOut->addHTML( $prevnext );
+               }
+       }
+
+       protected function listStart() {
+               return Xml::openElement( 'ul' );
+       }
+
+       protected function listItem( $row, $nt, $notClose = false ) {
+               # local message cache
+               static $msgcache = null;
+               if ( $msgcache === null ) {
+                       static $msgs = array( 'isredirect', 'istemplate', 'semicolon-separator',
+                               'whatlinkshere-links', 'isimage' );
+                       $msgcache = array();
+                       foreach ( $msgs as $msg ) {
+                               $msgcache[$msg] = wfMsgHtml( $msg );
+                       }
+               }
+
+               $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
+               $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
+
+               // Display properties (redirect or template)
+               $propsText = '';
+               $props = array();
+               if ( $row->page_is_redirect )
+                       $props[] = $msgcache['isredirect'];
+               if ( $row->is_template )
+                       $props[] = $msgcache['istemplate'];
+               if( $row->is_image )
+                       $props[] = $msgcache['isimage'];
+
+               if ( count( $props ) ) {
+                       $propsText = '(' . implode( $msgcache['semicolon-separator'], $props ) . ')';
+               }
+
+               # Space for utilities links, with a what-links-here link provided
+               $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'] );
+               $wlh = Xml::wrapClass( "($wlhLink)", 'mw-whatlinkshere-tools' );
+
+               return $notClose ?
+                       Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
+                       Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
+       }
+
+       protected function listEnd() {
+               return Xml::closeElement( 'ul' );
+       }
+
+       protected function wlhLink( Title $target, $text ) {
+               static $title = null;
+               if ( $title === null )
+                       $title = SpecialPage::getTitleFor( 'Whatlinkshere' );
+
+               $targetText = $target->getPrefixedUrl();
+               return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
+       }
+
+       function makeSelfLink( $text, $query ) {
+               return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
+       }
+
+       function getPrevNext( $prevId, $nextId ) {
+               global $wgLang;
+               $currentLimit = $this->opts->getValue( 'limit' );
+               $fmtLimit = $wgLang->formatNum( $currentLimit );
+               $prev = wfMsgExt( 'whatlinkshere-prev', array( 'parsemag', 'escape' ), $fmtLimit );
+               $next = wfMsgExt( 'whatlinkshere-next', array( 'parsemag', 'escape' ), $fmtLimit );
+
+               $changed = $this->opts->getChangedValues();
+               unset($changed['target']); // Already in the request title
+
+               if ( 0 != $prevId ) {
+                       $overrides = array( 'from' => $this->opts->getValue( 'back' ) );
+                       $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
+               }
+               if ( 0 != $nextId ) {
+                       $overrides = array( 'from' => $nextId, 'back' => $prevId );
+                       $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
+               }
+
+               $limitLinks = array();
+               foreach ( $this->limits as $limit ) {
+                       $prettyLimit = $wgLang->formatNum( $limit );
+                       $overrides = array( 'limit' => $limit );
+                       $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
+               }
+
+               $nums = implode ( ' | ', $limitLinks );
+
+               return wfMsgHtml( 'viewprevnext', $prev, $next, $nums );
+       }
+
+       function whatlinkshereForm() {
+               global $wgScript, $wgTitle;
+
+               // We get nicer value from the title object
+               $this->opts->consumeValue( 'target' );
+               // Reset these for new requests
+               $this->opts->consumeValues( array( 'back', 'from' ) );
+
+               $target = $this->target ? $this->target->getPrefixedText() : '';
+               $namespace = $this->opts->consumeValue( 'namespace' );
+
+               # Build up the form
+               $f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
+               
+               # Values that should not be forgotten
+               $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
+               foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
+                       $f .= Xml::hidden( $name, $value );
+               }
+
+               $f .= Xml::fieldset( wfMsg( 'whatlinkshere' ) );
+
+               # Target input
+               $f .= Xml::inputLabel( wfMsg( 'whatlinkshere-page' ), 'target',
+                               'mw-whatlinkshere-target', 40, $target );
+
+               $f .= ' ';
+
+               # Namespace selector
+               $f .= Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '&nbsp;' .
+                       Xml::namespaceSelector( $namespace, '' );
+
+               # Submit
+               $f .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+
+               # Close
+               $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
+
+               return $f;
+       }
+
+       function getFilterPanel() {
+               $show = wfMsgHtml( 'show' );
+               $hide = wfMsgHtml( 'hide' );
+
+               $changed = $this->opts->getChangedValues();
+               unset($changed['target']); // Already in the request title
+
+               $links = array();
+               $types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
+               if( $this->target->getNamespace() == NS_IMAGE )
+                       $types[] = 'hideimages';
+               foreach( $types as $type ) {
+                       $chosen = $this->opts->getValue( $type );
+                       $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
+                       $overrides = array( $type => !$chosen );
+                       $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
+               }
+               return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), implode( '&nbsp;|&nbsp;', $links ) );
+       }
+}
diff --git a/includes/specials/Withoutinterwiki.php b/includes/specials/Withoutinterwiki.php
new file mode 100644 (file)
index 0000000..2092e43
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+/**
+ * @file
+ * @ingroup SpecialPage
+ */
+
+/**
+ * Special page lists pages without language links
+ *
+ * @ingroup SpecialPage
+ * @author Rob Church <robchur@gmail.com>
+ */
+class WithoutInterwikiPage extends PageQueryPage {
+       private $prefix = '';
+
+       function getName() {
+               return 'Withoutinterwiki';
+       }
+
+       function getPageHeader() {
+               global $wgScript, $wgMiserMode;
+
+               # Do not show useless input form if wiki is running in misermode
+               if( $wgMiserMode ) {
+                       return '';
+               }
+
+               $prefix = $this->prefix;
+               $t = SpecialPage::getTitleFor( $this->getName() );
+
+               return  Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
+                       Xml::openElement( 'fieldset' ) .
+                       Xml::element( 'legend', null, wfMsg( 'withoutinterwiki-legend' ) ) .
+                       Xml::hidden( 'title', $t->getPrefixedText() ) .
+                       Xml::inputLabel( wfMsg( 'allpagesprefix' ), 'prefix', 'wiprefix', 20, $prefix ) . ' ' .
+                       Xml::submitButton( wfMsg( 'withoutinterwiki-submit' ) ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::closeElement( 'form' );
+       }
+
+       function sortDescending() {
+               return false;
+       }
+
+       function isExpensive() {
+               return true;
+       }
+
+       function isSyndicated() {
+               return false;
+       }
+
+       function getSQL() {
+               $dbr = wfGetDB( DB_SLAVE );
+               list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
+               $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
+               return
+                 "SELECT 'Withoutinterwiki'  AS type,
+                         page_namespace AS namespace,
+                         page_title     AS title,
+                         page_title     AS value
+                    FROM $page
+               LEFT JOIN $langlinks
+                      ON ll_from = page_id
+                   WHERE ll_title IS NULL
+                     AND page_namespace=" . NS_MAIN . "
+                     AND page_is_redirect = 0
+                         {$prefix}";
+       }
+
+       function setPrefix( $prefix = '' ) {
+               $this->prefix = $prefix;
+       }
+
+}
+
+function wfSpecialWithoutinterwiki() {
+       global $wgRequest, $wgContLang, $wgCapitalLinks;
+       list( $limit, $offset ) = wfCheckLimits();
+       if( $wgCapitalLinks ) {
+               $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) );
+       } else {
+               $prefix = $wgRequest->getVal( 'prefix' );
+       }
+       $wip = new WithoutInterwikiPage();
+       $wip->setPrefix( $prefix );
+       $wip->doQuery( $offset, $limit );
+}
index 4c7606d..4a2078c 100644 (file)
@@ -6,9 +6,6 @@
  */
 if( !defined( 'MEDIAWIKI' ) ) die( -1 );
 
-/** */
-require_once( 'includes/SkinTemplate.php' );
-
 /**
  * HTML template for Special:Userlogin form
  * @ingroup Templates
diff --git a/maintenance/checkAutoLoader.php b/maintenance/checkAutoLoader.php
new file mode 100644 (file)
index 0000000..c2909ef
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+if ( php_sapi_name() != 'cli' ) exit;
+
+$IP = dirname(__FILE__) .'/..';
+require( "$IP/includes/AutoLoader.php" );
+$files = array_unique( AutoLoader::$localClasses );
+
+foreach ( $files as $file ) {
+       $parseInfo = parsekit_compile_file( "$IP/$file" );
+       $classes = array_keys( $parseInfo['class_table'] );
+       foreach ( $classes as $class ) {
+               if ( !isset( AutoLoader::$localClasses[$class] ) ) {
+                       //printf( "%-50s Unlisted, in %s\n", $class, $file );
+                       echo "          '$class' => '$file',\n";
+               } elseif ( AutoLoader::$localClasses[$class] !== $file ) {
+                       echo "$class: Wrong file: found in $file, listed in " . AutoLoader::$localClasses[$class] . "\n";
+               }
+       }
+
+}
+
+
index bc75f9a..a23bb6e 100644 (file)
@@ -116,6 +116,7 @@ if ( file_exists( '/home/wikipedia/common/langlist' ) ) {
        $wgWikiFarm = true;
        #$cluster = trim( file_get_contents( '/etc/cluster' ) );
        $cluster = 'pmtpa';
+       require_once( "$IP/includes/AutoLoader.php" );
        require_once( "$IP/includes/SiteConfiguration.php" );
 
        # Get $wgConf
@@ -199,6 +200,7 @@ if ( file_exists( '/home/wikipedia/common/langlist' ) ) {
        }
        $wgCommandLineMode = true;
        $DP = $IP;
+       require_once( "$IP/includes/AutoLoader.php" );
        #require_once( $IP.'/includes/ProfilerStub.php' );
        require_once( $IP.'/includes/Defines.php' );
        require_once( $settingsFile );