From 086ae52d12011746a75f5588e877347bc0457352 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 21 Mar 2008 11:49:34 +0100 Subject: Update auf MediaWiki 1.12.0 --- HISTORY | 596 ++- README | 8 +- RELEASE-NOTES | 1351 +++--- UPGRADE | 2 +- api.php5 | 2 +- config/index.php | 63 +- docs/globals.txt | 10 + docs/hooks.txt | 387 +- extensions/LLAuthPlugin.php | 4 + img_auth.php | 9 +- img_auth.php5 | 2 +- includes/AjaxFunctions.php | 121 +- includes/Article.php | 718 +-- includes/AuthPlugin.php | 16 +- includes/AutoLoader.php | 55 +- includes/Autopromote.php | 113 + includes/BagOStuff.php | 35 + includes/CategoryPage.php | 17 +- includes/ChangesList.php | 14 +- includes/CoreParserFunctions.php | 77 +- includes/Database.php | 52 +- includes/DatabasePostgres.php | 563 ++- includes/DefaultSettings.php | 243 +- includes/Defines.php | 27 +- includes/DifferenceEngine.php | 256 +- includes/EditPage.php | 594 ++- includes/Exception.php | 42 +- includes/Export.php | 2 +- includes/ExternalEdit.php | 8 +- includes/ExternalStore.php | 9 +- includes/ExternalStoreHttp.php | 6 +- includes/Feed.php | 6 +- includes/FileDeleteForm.php | 88 +- includes/FileRevertForm.php | 9 +- includes/FileStore.php | 2 +- includes/GlobalFunctions.php | 276 +- includes/HTMLCacheUpdate.php | 22 +- includes/ImageGallery.php | 2 +- includes/ImagePage.php | 72 +- includes/JobQueue.php | 3 +- includes/LinkBatch.php | 2 +- includes/LinkFilter.php | 9 +- includes/Linker.php | 348 +- includes/LinksUpdate.php | 11 + includes/LoadBalancer.php | 28 +- includes/LogPage.php | 16 +- includes/MagicWord.php | 52 +- includes/Math.php | 15 +- includes/MessageCache.php | 149 +- includes/MimeMagic.php | 318 +- includes/Namespace.php | 5 + includes/OutputHandler.php | 84 +- includes/OutputPage.php | 371 +- includes/PageHistory.php | 97 +- includes/Pager.php | 24 +- includes/Parser.php | 2143 +++++---- includes/ParserOptions.php | 26 + includes/ParserOutput.php | 23 +- includes/Parser_DiffTest.php | 85 + includes/Parser_OldPP.php | 4942 ++++++++++++++++++++ includes/PrefixSearch.php | 135 + includes/Preprocessor.php | 154 + includes/Preprocessor_DOM.php | 1356 ++++++ includes/Preprocessor_Hash.php | 1471 ++++++ includes/ProfilerSimple.php | 4 + includes/ProtectionForm.php | 59 +- includes/QueryPage.php | 11 +- includes/RawPage.php | 37 +- includes/RecentChange.php | 24 +- includes/Revision.php | 19 +- includes/Sanitizer.php | 2 +- includes/SearchEngine.php | 19 +- includes/SearchMySQL4.php | 10 +- includes/SearchPostgres.php | 27 +- includes/Setup.php | 5 +- includes/SiteConfiguration.php | 71 +- includes/Skin.php | 103 +- includes/SkinTemplate.php | 87 +- includes/SpecialAllmessages.php | 33 +- includes/SpecialAllpages.php | 38 +- includes/SpecialBlockip.php | 149 +- includes/SpecialBlockme.php | 5 +- includes/SpecialBooksources.php | 4 +- includes/SpecialBrokenRedirects.php | 2 +- includes/SpecialCategories.php | 4 +- includes/SpecialConfirmemail.php | 18 +- includes/SpecialContributions.php | 48 +- includes/SpecialDoubleRedirects.php | 4 +- includes/SpecialEmailuser.php | 47 +- includes/SpecialExport.php | 96 +- includes/SpecialFilepath.php | 69 + includes/SpecialImport.php | 156 +- includes/SpecialIpblocklist.php | 100 +- includes/SpecialListredirects.php | 15 +- includes/SpecialLockdb.php | 4 +- includes/SpecialLog.php | 37 +- includes/SpecialMIMEsearch.php | 2 +- includes/SpecialMergeHistory.php | 423 ++ includes/SpecialMostlinked.php | 2 +- includes/SpecialMostlinkedcategories.php | 2 +- includes/SpecialMostlinkedtemplates.php | 16 +- includes/SpecialMovepage.php | 42 +- includes/SpecialNewimages.php | 2 +- includes/SpecialNewpages.php | 309 +- includes/SpecialPage.php | 80 +- includes/SpecialPreferences.php | 82 +- includes/SpecialPrefixindex.php | 10 +- includes/SpecialProtectedpages.php | 3 +- includes/SpecialProtectedtitles.php | 219 + includes/SpecialRandompage.php | 66 +- includes/SpecialRandomredirect.php | 27 +- includes/SpecialRecentchanges.php | 69 +- includes/SpecialRecentchangeslinked.php | 23 +- includes/SpecialResetpass.php | 4 +- includes/SpecialRevisiondelete.php | 4 +- includes/SpecialSearch.php | 41 +- includes/SpecialShortpages.php | 2 +- includes/SpecialSpecialpages.php | 2 +- includes/SpecialStatistics.php | 10 +- includes/SpecialUndelete.php | 248 +- includes/SpecialUnlockdb.php | 6 +- includes/SpecialUnusedimages.php | 22 +- includes/SpecialUpload.php | 339 +- includes/SpecialUserlogin.php | 91 +- includes/SpecialUserlogout.php | 15 +- includes/SpecialUserrights.php | 431 +- includes/SpecialVersion.php | 165 +- includes/SpecialWantedcategories.php | 2 +- includes/SpecialWantedpages.php | 2 +- includes/SpecialWatchlist.php | 50 +- includes/SpecialWhatlinkshere.php | 66 +- includes/SpecialWithoutinterwiki.php | 41 +- includes/SquidUpdate.php | 2 +- includes/StreamFile.php | 6 +- includes/StubObject.php | 3 +- includes/Title.php | 542 ++- includes/User.php | 321 +- includes/UserMailer.php | 461 +- includes/UserRightsProxy.php | 161 + includes/WatchlistEditor.php | 40 +- includes/WebRequest.php | 44 +- includes/Wiki.php | 35 +- includes/WikiError.php | 8 +- includes/Xml.php | 67 +- includes/XmlTypeCheck.php | 93 + includes/ZhConversion.php | 1723 +++---- includes/api/ApiBase.php | 186 +- includes/api/ApiBlock.php | 164 + includes/api/ApiChangeRights.php | 155 + includes/api/ApiDelete.php | 155 + includes/api/ApiExpandTemplates.php | 97 + includes/api/ApiFeedWatchlist.php | 29 +- includes/api/ApiFormatBase.php | 52 +- includes/api/ApiFormatDbg.php | 59 + includes/api/ApiFormatJson.php | 8 +- includes/api/ApiFormatPhp.php | 4 +- includes/api/ApiFormatTxt.php | 59 + includes/api/ApiFormatWddx.php | 4 +- includes/api/ApiFormatXml.php | 4 +- includes/api/ApiFormatYaml.php | 4 +- includes/api/ApiFormatYaml_spyc.php | 14 +- includes/api/ApiHelp.php | 8 +- includes/api/ApiLogin.php | 22 +- includes/api/ApiLogout.php | 71 + includes/api/ApiMain.php | 112 +- includes/api/ApiMove.php | 152 + includes/api/ApiOpenSearch.php | 51 +- includes/api/ApiParamInfo.php | 167 + includes/api/ApiParse.php | 202 + includes/api/ApiProtect.php | 154 + includes/api/ApiQuery.php | 26 +- includes/api/ApiQueryAllCategories.php | 142 + includes/api/ApiQueryAllLinks.php | 8 +- includes/api/ApiQueryAllUsers.php | 17 +- includes/api/ApiQueryAllmessages.php | 129 + includes/api/ApiQueryAllpages.php | 48 +- includes/api/ApiQueryBacklinks.php | 10 +- includes/api/ApiQueryBase.php | 7 +- includes/api/ApiQueryBlocks.php | 239 + includes/api/ApiQueryCategories.php | 8 +- includes/api/ApiQueryCategoryMembers.php | 61 +- includes/api/ApiQueryDeletedrevs.php | 235 + includes/api/ApiQueryExtLinksUsage.php | 8 +- includes/api/ApiQueryExternalLinks.php | 4 +- includes/api/ApiQueryImageInfo.php | 170 +- includes/api/ApiQueryImages.php | 4 +- includes/api/ApiQueryInfo.php | 113 +- includes/api/ApiQueryLangLinks.php | 4 +- includes/api/ApiQueryLinks.php | 8 +- includes/api/ApiQueryLogEvents.php | 9 +- includes/api/ApiQueryRandom.php | 157 + includes/api/ApiQueryRecentChanges.php | 110 +- includes/api/ApiQueryRevisions.php | 77 +- includes/api/ApiQuerySearch.php | 8 +- includes/api/ApiQuerySiteinfo.php | 39 +- includes/api/ApiQueryUserContributions.php | 36 +- includes/api/ApiQueryUserInfo.php | 138 +- includes/api/ApiQueryUsers.php | 162 + includes/api/ApiQueryWatchlist.php | 50 +- includes/api/ApiResult.php | 45 +- includes/api/ApiRollback.php | 128 + includes/api/ApiUnblock.php | 124 + includes/api/ApiUndelete.php | 123 + includes/filerepo/ArchivedFile.php | 306 +- includes/filerepo/FSRepo.php | 2 +- includes/filerepo/File.php | 75 +- includes/filerepo/FileRepo.php | 25 +- includes/filerepo/FileRepoStatus.php | 12 +- includes/filerepo/ICRepo.php | 204 +- includes/filerepo/LocalFile.php | 128 +- includes/filerepo/LocalRepo.php | 48 + includes/filerepo/NullRepo.php | 34 + includes/filerepo/RepoGroup.php | 47 +- includes/media/Generic.php | 36 +- includes/mime.info | 1 + includes/mime.types | 2 +- includes/templates/Userlogin.php | 51 +- includes/zhtable/Makefile | 86 +- includes/zhtable/toCN.manual | 41 +- includes/zhtable/toHK.manual | 34 +- includes/zhtable/toTW.manual | 41 +- index.php | 7 +- index.php5 | 2 +- install-utils.inc | 4 +- languages/Language.php | 563 ++- languages/LanguageConverter.php | 9 +- languages/Names.php | 127 +- languages/classes/LanguageAr.php | 6 +- languages/classes/LanguageBat_smg.php | 26 + languages/classes/LanguageBe.php | 15 +- languages/classes/LanguageBe_tarask.php | 29 +- languages/classes/LanguageBs.php | 14 +- languages/classes/LanguageCs.php | 50 +- languages/classes/LanguageCu.php | 15 +- languages/classes/LanguageCy.php | 21 + languages/classes/LanguageDsb.php | 115 + languages/classes/LanguageFi.php | 6 +- languages/classes/LanguageFr.php | 10 +- languages/classes/LanguageHe.php | 13 +- languages/classes/LanguageHr.php | 15 +- languages/classes/LanguageHsb.php | 17 +- languages/classes/LanguageHy.php | 12 +- languages/classes/LanguageKaa.php | 43 + languages/classes/LanguageKk.deps.php | 3 +- languages/classes/LanguageKk.php | 498 +- languages/classes/LanguageKk_cyrl.php | 722 +++ languages/classes/LanguageKsh.php | 16 +- languages/classes/LanguageKu.php | 32 +- languages/classes/LanguageLt.php | 16 +- languages/classes/LanguageLv.php | 7 +- languages/classes/LanguagePl.php | 24 + languages/classes/LanguagePt_br.php | 9 +- languages/classes/LanguageRu.php | 34 +- languages/classes/LanguageSk.php | 7 +- languages/classes/LanguageSl.php | 7 +- languages/classes/LanguageSr_ec.php | 14 +- languages/classes/LanguageSr_el.php | 14 +- languages/classes/LanguageUk.php | 14 +- languages/classes/LanguageYue.php | 23 + languages/classes/LanguageZh.deps.php | 3 +- languages/classes/LanguageZh.php | 56 +- languages/classes/LanguageZh_hans.php | 23 + languages/messages/MessagesAb.php | 6 +- languages/messages/MessagesAf.php | 821 +++- languages/messages/MessagesAk.php | 58 + languages/messages/MessagesAln.php | 421 ++ languages/messages/MessagesAls.php | 8 + languages/messages/MessagesAm.php | 1089 +++++ languages/messages/MessagesAn.php | 2362 +++++++++- languages/messages/MessagesAng.php | 669 +++ languages/messages/MessagesAr.php | 1647 ++++--- languages/messages/MessagesArc.php | 221 +- languages/messages/MessagesArn.php | 297 ++ languages/messages/MessagesAs.php | 40 + languages/messages/MessagesAst.php | 2387 +++++++++- languages/messages/MessagesAv.php | 114 +- languages/messages/MessagesAvk.php | 1865 ++++++++ languages/messages/MessagesAy.php | 131 +- languages/messages/MessagesAz.php | 89 +- languages/messages/MessagesBa.php | 106 +- languages/messages/MessagesBar.php | 103 +- languages/messages/MessagesBat_smg.php | 152 + languages/messages/MessagesBcl.php | 1982 ++++++-- languages/messages/MessagesBe.php | 1817 +++++-- languages/messages/MessagesBe_tarask.php | 640 ++- languages/messages/MessagesBe_x_old.php | 1026 +--- languages/messages/MessagesBg.php | 656 ++- languages/messages/MessagesBh.php | 10 +- languages/messages/MessagesBi.php | 147 + languages/messages/MessagesBm.php | 122 +- languages/messages/MessagesBn.php | 2300 ++++++++- languages/messages/MessagesBo.php | 7 +- languages/messages/MessagesBpy.php | 273 +- languages/messages/MessagesBr.php | 1060 +++-- languages/messages/MessagesBs.php | 94 +- languages/messages/MessagesBug.php | 107 + languages/messages/MessagesCa.php | 1364 ++++-- languages/messages/MessagesCbk_zam.php | 62 + languages/messages/MessagesCdo.php | 921 ++++ languages/messages/MessagesCe.php | 287 +- languages/messages/MessagesCeb.php | 110 + languages/messages/MessagesChr.php | 72 + languages/messages/MessagesCo.php | 501 ++ languages/messages/MessagesCrh.php | 12 + languages/messages/MessagesCrh_cyrl.php | 1805 +++++++ languages/messages/MessagesCrh_latn.php | 1802 +++++++ languages/messages/MessagesCs.php | 1367 ++++-- languages/messages/MessagesCsb.php | 725 ++- languages/messages/MessagesCu.php | 130 +- languages/messages/MessagesCv.php | 45 +- languages/messages/MessagesCy.php | 1914 ++++++-- languages/messages/MessagesDa.php | 623 +-- languages/messages/MessagesDe.php | 819 ++-- languages/messages/MessagesDe_formal.php | 485 ++ languages/messages/MessagesDiq.php | 365 ++ languages/messages/MessagesDk.php | 8 + languages/messages/MessagesDsb.php | 2440 ++++++++++ languages/messages/MessagesDum.php | 120 + languages/messages/MessagesDv.php | 398 +- languages/messages/MessagesDz.php | 14 +- languages/messages/MessagesEe.php | 201 + languages/messages/MessagesEl.php | 920 ++-- languages/messages/MessagesEml.php | 151 + languages/messages/MessagesEn.php | 1038 ++-- languages/messages/MessagesEnRTL.php | 6 +- languages/messages/MessagesEo.php | 1173 +++-- languages/messages/MessagesEs.php | 897 ++-- languages/messages/MessagesEt.php | 183 +- languages/messages/MessagesEu.php | 437 +- languages/messages/MessagesExt.php | 2121 +++++++-- languages/messages/MessagesFa.php | 1080 +++-- languages/messages/MessagesFf.php | 59 + languages/messages/MessagesFi.php | 623 ++- languages/messages/MessagesFiu_vro.php | 316 +- languages/messages/MessagesFj.php | 202 + languages/messages/MessagesFo.php | 1178 ++++- languages/messages/MessagesFr.php | 940 ++-- languages/messages/MessagesFrc.php | 1242 ++--- languages/messages/MessagesFrp.php | 1078 +++-- languages/messages/MessagesFur.php | 504 +- languages/messages/MessagesFy.php | 1121 +++-- languages/messages/MessagesGa.php | 905 ++-- languages/messages/MessagesGag.php | 900 ++++ languages/messages/MessagesGan.php | 2157 +++++++++ languages/messages/MessagesGd.php | 386 ++ languages/messages/MessagesGl.php | 2191 +++++++-- languages/messages/MessagesGlk.php | 36 + languages/messages/MessagesGn.php | 20 + languages/messages/MessagesGot.php | 86 + languages/messages/MessagesGrc.php | 510 ++ languages/messages/MessagesGsw.php | 136 +- languages/messages/MessagesGu.php | 406 +- languages/messages/MessagesHak.php | 154 +- languages/messages/MessagesHaw.php | 401 ++ languages/messages/MessagesHe.php | 1088 +++-- languages/messages/MessagesHi.php | 136 +- languages/messages/MessagesHr.php | 1431 ++++-- languages/messages/MessagesHsb.php | 673 ++- languages/messages/MessagesHt.php | 641 ++- languages/messages/MessagesHu.php | 2688 ++++++++--- languages/messages/MessagesHy.php | 2092 +++++++-- languages/messages/MessagesIa.php | 128 +- languages/messages/MessagesId.php | 448 +- languages/messages/MessagesIe.php | 251 + languages/messages/MessagesIg.php | 30 + languages/messages/MessagesIi.php | 9 +- languages/messages/MessagesIk.php | 15 + languages/messages/MessagesIke_cans.php | 402 ++ languages/messages/MessagesIke_latn.php | 369 ++ languages/messages/MessagesIlo.php | 269 ++ languages/messages/MessagesInh.php | 221 + languages/messages/MessagesIo.php | 865 ++++ languages/messages/MessagesIs.php | 1106 +++-- languages/messages/MessagesIt.php | 493 +- languages/messages/MessagesIu.php | 10 + languages/messages/MessagesJa.php | 695 ++- languages/messages/MessagesJbo.php | 177 +- languages/messages/MessagesJut.php | 637 +++ languages/messages/MessagesJv.php | 100 +- languages/messages/MessagesKa.php | 1744 +++++-- languages/messages/MessagesKaa.php | 1521 +++++- languages/messages/MessagesKab.php | 956 ++-- languages/messages/MessagesKg.php | 160 +- languages/messages/MessagesKk.php | 42 +- languages/messages/MessagesKk_arab.php | 2786 +++++++++++ languages/messages/MessagesKk_cn.php | 2699 +---------- languages/messages/MessagesKk_cyrl.php | 2759 +++++++++++ languages/messages/MessagesKk_kz.php | 2691 +---------- languages/messages/MessagesKk_latn.php | 2759 +++++++++++ languages/messages/MessagesKk_tr.php | 2692 +---------- languages/messages/MessagesKl.php | 139 + languages/messages/MessagesKm.php | 1598 ++++++- languages/messages/MessagesKn.php | 908 +++- languages/messages/MessagesKo.php | 573 ++- languages/messages/MessagesKrj.php | 366 +- languages/messages/MessagesKs.php | 11 +- languages/messages/MessagesKsh.php | 258 +- languages/messages/MessagesKu.php | 11 +- languages/messages/MessagesKu_arab.php | 337 +- languages/messages/MessagesKu_latn.php | 1435 ++++-- languages/messages/MessagesKv.php | 7 +- languages/messages/MessagesKw.php | 118 + languages/messages/MessagesKy.php | 331 ++ languages/messages/MessagesLa.php | 885 ++-- languages/messages/MessagesLad.php | 102 + languages/messages/MessagesLb.php | 2068 ++++++++ languages/messages/MessagesLbe.php | 80 + languages/messages/MessagesLfn.php | 821 ++++ languages/messages/MessagesLg.php | 482 +- languages/messages/MessagesLi.php | 2076 ++++++-- languages/messages/MessagesLij.php | 923 ++++ languages/messages/MessagesLld.php | 9 + languages/messages/MessagesLmo.php | 524 +++ languages/messages/MessagesLn.php | 131 +- languages/messages/MessagesLo.php | 112 +- languages/messages/MessagesLoz.php | 1127 +++++ languages/messages/MessagesLt.php | 499 +- languages/messages/MessagesLv.php | 91 +- languages/messages/MessagesMai.php | 267 ++ languages/messages/MessagesMdf.php | 1594 +++++++ languages/messages/MessagesMg.php | 1096 +++++ languages/messages/MessagesMi.php | 43 +- languages/messages/MessagesMk.php | 162 +- languages/messages/MessagesMl.php | 1950 +++++++- languages/messages/MessagesMn.php | 110 +- languages/messages/MessagesMo.php | 150 + languages/messages/MessagesMr.php | 2349 +++++++++- languages/messages/MessagesMs.php | 2849 ++++++++--- languages/messages/MessagesMt.php | 53 +- languages/messages/MessagesMy.php | 23 +- languages/messages/MessagesMyv.php | 1005 ++++ languages/messages/MessagesMzn.php | 13 +- languages/messages/MessagesNa.php | 116 + languages/messages/MessagesNah.php | 115 +- languages/messages/MessagesNan.php | 867 ++++ languages/messages/MessagesNap.php | 428 +- languages/messages/MessagesNb.php | 9 + languages/messages/MessagesNds.php | 814 +++- languages/messages/MessagesNds_nl.php | 335 +- languages/messages/MessagesNe.php | 610 ++- languages/messages/MessagesNew.php | 108 +- languages/messages/MessagesNl.php | 1640 ++++--- languages/messages/MessagesNn.php | 1647 +++++-- languages/messages/MessagesNo.php | 884 ++-- languages/messages/MessagesNon.php | 25 +- languages/messages/MessagesNov.php | 371 ++ languages/messages/MessagesNso.php | 1076 +++++ languages/messages/MessagesNv.php | 67 +- languages/messages/MessagesNy.php | 27 + languages/messages/MessagesOc.php | 724 ++- languages/messages/MessagesOr.php | 12 +- languages/messages/MessagesOs.php | 32 +- languages/messages/MessagesPa.php | 72 +- languages/messages/MessagesPag.php | 401 ++ languages/messages/MessagesPam.php | 368 ++ languages/messages/MessagesPap.php | 379 ++ languages/messages/MessagesPdc.php | 100 + languages/messages/MessagesPfl.php | 179 + languages/messages/MessagesPi.php | 8 +- languages/messages/MessagesPih.php | 64 + languages/messages/MessagesPl.php | 1203 +++-- languages/messages/MessagesPms.php | 661 ++- languages/messages/MessagesPnt.php | 65 + languages/messages/MessagesPs.php | 1058 ++++- languages/messages/MessagesPt.php | 736 ++- languages/messages/MessagesPt_br.php | 1748 +++++-- languages/messages/MessagesQu.php | 2286 ++++++++- languages/messages/MessagesRm.php | 643 +++ languages/messages/MessagesRmy.php | 47 +- languages/messages/MessagesRo.php | 383 +- languages/messages/MessagesRoa_rup.php | 21 +- languages/messages/MessagesRu.php | 630 ++- languages/messages/MessagesRuq.php | 10 + languages/messages/MessagesRuq_cyrl.php | 318 ++ languages/messages/MessagesRuq_grek.php | 9 + languages/messages/MessagesRuq_latn.php | 318 ++ languages/messages/MessagesSa.php | 60 +- languages/messages/MessagesSah.php | 1350 +++++- languages/messages/MessagesSc.php | 143 +- languages/messages/MessagesScn.php | 2104 ++++++++- languages/messages/MessagesSco.php | 1213 +++++ languages/messages/MessagesSd.php | 124 +- languages/messages/MessagesSdc.php | 1977 ++++++++ languages/messages/MessagesSe.php | 2685 ++++++----- languages/messages/MessagesSei.php | 1143 +++++ languages/messages/MessagesSg.php | 55 + languages/messages/MessagesShi.php | 132 + languages/messages/MessagesSi.php | 267 ++ languages/messages/MessagesSimple.php | 9 + languages/messages/MessagesSk.php | 1056 +++-- languages/messages/MessagesSl.php | 169 +- languages/messages/MessagesSm.php | 223 + languages/messages/MessagesSma.php | 902 ++++ languages/messages/MessagesSn.php | 21 +- languages/messages/MessagesSo.php | 92 +- languages/messages/MessagesSq.php | 387 +- languages/messages/MessagesSr.php | 11 +- languages/messages/MessagesSr_ec.php | 1072 +++-- languages/messages/MessagesSr_el.php | 133 +- languages/messages/MessagesSr_jc.php | 9 +- languages/messages/MessagesSr_jl.php | 9 +- languages/messages/MessagesSrn.php | 1072 +++++ languages/messages/MessagesSs.php | 104 + languages/messages/MessagesSt.php | 155 + languages/messages/MessagesStq.php | 2313 +++++++++ languages/messages/MessagesSu.php | 737 ++- languages/messages/MessagesSv.php | 731 +-- languages/messages/MessagesSw.php | 753 +++ languages/messages/MessagesTa.php | 1741 +++++-- languages/messages/MessagesTe.php | 2344 ++++++++-- languages/messages/MessagesTet.php | 595 +++ languages/messages/MessagesTg.php | 2314 ++++++++- languages/messages/MessagesTh.php | 746 +-- languages/messages/MessagesTi.php | 26 +- languages/messages/MessagesTk.php | 17 + languages/messages/MessagesTl.php | 633 +++ languages/messages/MessagesTn.php | 25 +- languages/messages/MessagesTo.php | 1238 +++++ languages/messages/MessagesTokipona.php | 155 + languages/messages/MessagesTp.php | 9 + languages/messages/MessagesTpi.php | 30 +- languages/messages/MessagesTr.php | 1406 ++++-- languages/messages/MessagesTt.php | 81 +- languages/messages/MessagesTy.php | 163 +- languages/messages/MessagesTyv.php | 693 ++- languages/messages/MessagesUdm.php | 27 +- languages/messages/MessagesUg.php | 117 +- languages/messages/MessagesUk.php | 262 +- languages/messages/MessagesUr.php | 267 +- languages/messages/MessagesUz.php | 305 +- languages/messages/MessagesVe.php | 41 + languages/messages/MessagesVec.php | 220 +- languages/messages/MessagesVi.php | 2446 +++++++--- languages/messages/MessagesVls.php | 79 +- languages/messages/MessagesVo.php | 1892 +++++++- languages/messages/MessagesWa.php | 691 +-- languages/messages/MessagesWar.php | 401 +- languages/messages/MessagesWo.php | 1012 +++- languages/messages/MessagesWuu.php | 855 ++++ languages/messages/MessagesXal.php | 50 +- languages/messages/MessagesXh.php | 147 + languages/messages/MessagesXmf.php | 425 ++ languages/messages/MessagesYdd.php | 10 + languages/messages/MessagesYi.php | 438 +- languages/messages/MessagesYo.php | 146 + languages/messages/MessagesYue.php | 2410 ++++++++++ languages/messages/MessagesZa.php | 154 +- languages/messages/MessagesZea.php | 360 +- languages/messages/MessagesZh.php | 22 +- languages/messages/MessagesZh_classical.php | 1719 ++++--- languages/messages/MessagesZh_cn.php | 2262 +-------- languages/messages/MessagesZh_hans.php | 2382 ++++++++++ languages/messages/MessagesZh_hant.php | 2354 ++++++++++ languages/messages/MessagesZh_hk.php | 42 +- languages/messages/MessagesZh_min_nan.php | 10 + languages/messages/MessagesZh_sg.php | 12 +- languages/messages/MessagesZh_tw.php | 1035 ++-- languages/messages/MessagesZh_yue.php | 2370 +--------- languages/messages/MessagesZu.php | 353 ++ locale/README | 2 +- maintenance/addwiki.php | 31 +- maintenance/archives/patch-image_reditects.sql | 0 maintenance/archives/patch-protected_titles.sql | 12 + maintenance/archives/populateSha1.php | 22 +- maintenance/backup.inc | 3 +- maintenance/cleanupImages.php | 6 +- maintenance/cleanupSpam.php | 3 +- maintenance/cleanupTitles.php | 6 +- maintenance/commandLine.inc | 14 + maintenance/createAndPromote.php | 2 +- maintenance/deleteBatch.php | 6 +- maintenance/deleteDefaultMessages.php | 4 +- maintenance/deleteOldRevisions.inc | 17 +- maintenance/deleteOldRevisions.php | 4 +- maintenance/dumpHTML.php | 163 +- maintenance/dumpTextPass.php | 150 +- maintenance/dumpUploads.php | 95 +- maintenance/fetchText.php | 36 + maintenance/findhooks.php | 98 +- maintenance/interwiki.sql | 4 +- maintenance/language/StatOutputs.php | 103 + maintenance/language/checkLanguage.inc | 128 +- maintenance/language/checkLanguage.php | 323 +- maintenance/language/lang2po.php | 14 +- maintenance/language/languages.inc | 113 +- maintenance/language/messageTypes.inc | 70 + maintenance/language/messages.inc | 297 +- maintenance/language/rebuildLanguage.php | 2 +- maintenance/language/splitLanguageFiles.inc | 1 - maintenance/language/transstat.php | 96 +- maintenance/language/writeMessagesArray.inc | 360 +- maintenance/namespaceDupes.php | 54 +- maintenance/nextJobDB.php | 6 +- maintenance/parserTests.inc | 64 +- maintenance/parserTests.php | 1 + maintenance/parserTests.txt | 442 +- .../postgres/archives/patch-protected_titles.sql | 10 + .../postgres/archives/patch-ts2pagetitle.sql | 13 + maintenance/postgres/compare_schemas.pl | 221 +- maintenance/postgres/mediawiki_mysql2postgres.pl | 11 +- maintenance/postgres/tables.sql | 112 +- maintenance/preprocessorFuzzTest.php | 233 + maintenance/rebuildInterwiki.inc | 9 +- maintenance/rebuildInterwiki.php | 16 +- maintenance/rebuildall.php | 3 +- maintenance/rebuildmessages.php | 17 + maintenance/rebuildrecentchanges.inc | 34 +- maintenance/rebuildrecentchanges.php | 4 +- maintenance/refreshLinks.inc | 2 +- maintenance/refreshLinks.php | 18 +- maintenance/runJobs.php | 8 +- maintenance/storage/compressOld.inc | 2 +- maintenance/tables.sql | 13 + maintenance/testRunner.postgres.sql | 30 + maintenance/updateRestrictions.php | 2 +- maintenance/updateSpecialPages.php | 7 +- maintenance/updaters.inc | 466 +- maintenance/wikipedia-interwiki.sql | 2 +- math/render.ml | 2 +- opensearch_desc.php | 10 +- opensearch_desc.php5 | 1 + profileinfo.php | 15 +- redirect.php5 | 2 +- skins/ArchLinux.php | 14 +- skins/Modern.deps.php | 13 + skins/Modern.php | 293 ++ skins/MonoBook.php | 12 +- skins/archlinux/IE60Fixes.css | 5 +- skins/archlinux/KHTMLFixes.css | 1 + skins/archlinux/headbg.jpg | Bin 0 -> 7881 bytes skins/archlinux/main.css | 60 +- skins/archlinux/rtl.css | 9 + skins/archlinux/user.gif | Bin 932 -> 923 bytes skins/archlinux/wiki-indexed.png | Bin 0 -> 8205 bytes skins/archlinux/wiki.png | Bin 0 -> 23064 bytes skins/chick/main.css | 3 +- skins/common/ajaxsearch.js | 1 - skins/common/block.js | 29 +- skins/common/cologneblue.css | 4 +- skins/common/commonPrint.css | 3 +- skins/common/oldshared.css | 3 +- skins/common/preview.js | 109 +- skins/common/protect.js | 68 +- skins/common/shared.css | 31 + skins/common/upload.js | 2 +- skins/common/wikibits.js | 4 +- skins/disabled/MonoBookCBT.php | 2 +- skins/modern/audio.png | Bin 0 -> 312 bytes skins/modern/bullet.gif | Bin 0 -> 50 bytes skins/modern/discussionitem_icon.gif | Bin 0 -> 949 bytes skins/modern/document.png | Bin 0 -> 270 bytes skins/modern/external.png | Bin 0 -> 165 bytes skins/modern/file_icon.gif | Bin 0 -> 921 bytes skins/modern/footer-grad.png | Bin 0 -> 149 bytes skins/modern/link_icon.gif | Bin 0 -> 942 bytes skins/modern/lock_icon.gif | Bin 0 -> 918 bytes skins/modern/mail_icon.gif | Bin 0 -> 918 bytes skins/modern/main.css | 1119 +++++ skins/modern/news_icon.png | Bin 0 -> 297 bytes skins/modern/print.css | 9 + skins/modern/rtl.css | 142 + skins/modern/video.png | Bin 0 -> 215 bytes skins/monobook/IE60Fixes.css | 5 +- skins/monobook/KHTMLFixes.css | 1 + skins/monobook/main.css | 60 +- skins/monobook/rtl.css | 9 + skins/simple/main.css | 17 +- skins/simple/rtl.css | 175 + thumb.php | 162 +- thumb.php5 | 2 +- trackback.php | 6 +- 671 files changed, 194580 insertions(+), 52279 deletions(-) create mode 100644 includes/Autopromote.php create mode 100644 includes/Parser_DiffTest.php create mode 100644 includes/Parser_OldPP.php create mode 100644 includes/PrefixSearch.php create mode 100644 includes/Preprocessor.php create mode 100644 includes/Preprocessor_DOM.php create mode 100644 includes/Preprocessor_Hash.php create mode 100644 includes/SpecialFilepath.php create mode 100644 includes/SpecialMergeHistory.php create mode 100755 includes/SpecialProtectedtitles.php create mode 100644 includes/UserRightsProxy.php create mode 100644 includes/XmlTypeCheck.php create mode 100644 includes/api/ApiBlock.php create mode 100644 includes/api/ApiChangeRights.php create mode 100644 includes/api/ApiDelete.php create mode 100644 includes/api/ApiExpandTemplates.php create mode 100644 includes/api/ApiFormatDbg.php create mode 100644 includes/api/ApiFormatTxt.php create mode 100644 includes/api/ApiLogout.php create mode 100644 includes/api/ApiMove.php create mode 100644 includes/api/ApiParamInfo.php create mode 100644 includes/api/ApiParse.php create mode 100644 includes/api/ApiProtect.php create mode 100644 includes/api/ApiQueryAllCategories.php create mode 100644 includes/api/ApiQueryAllmessages.php create mode 100644 includes/api/ApiQueryBlocks.php create mode 100644 includes/api/ApiQueryDeletedrevs.php create mode 100644 includes/api/ApiQueryRandom.php create mode 100644 includes/api/ApiQueryUsers.php create mode 100644 includes/api/ApiRollback.php create mode 100644 includes/api/ApiUnblock.php create mode 100644 includes/api/ApiUndelete.php create mode 100644 includes/filerepo/NullRepo.php create mode 100644 languages/classes/LanguageBat_smg.php create mode 100644 languages/classes/LanguageCy.php create mode 100644 languages/classes/LanguageDsb.php create mode 100644 languages/classes/LanguageKaa.php create mode 100644 languages/classes/LanguageKk_cyrl.php create mode 100644 languages/classes/LanguagePl.php create mode 100644 languages/classes/LanguageYue.php create mode 100644 languages/classes/LanguageZh_hans.php create mode 100644 languages/messages/MessagesAk.php create mode 100644 languages/messages/MessagesAln.php create mode 100644 languages/messages/MessagesAls.php create mode 100644 languages/messages/MessagesAm.php create mode 100644 languages/messages/MessagesAng.php create mode 100644 languages/messages/MessagesArn.php create mode 100644 languages/messages/MessagesAvk.php create mode 100644 languages/messages/MessagesBi.php create mode 100644 languages/messages/MessagesBug.php create mode 100644 languages/messages/MessagesCbk_zam.php create mode 100644 languages/messages/MessagesCdo.php create mode 100644 languages/messages/MessagesCeb.php create mode 100644 languages/messages/MessagesChr.php create mode 100644 languages/messages/MessagesCo.php create mode 100644 languages/messages/MessagesCrh.php create mode 100644 languages/messages/MessagesCrh_cyrl.php create mode 100644 languages/messages/MessagesCrh_latn.php create mode 100644 languages/messages/MessagesDe_formal.php create mode 100644 languages/messages/MessagesDiq.php create mode 100644 languages/messages/MessagesDk.php create mode 100644 languages/messages/MessagesDsb.php create mode 100644 languages/messages/MessagesDum.php create mode 100644 languages/messages/MessagesEe.php create mode 100644 languages/messages/MessagesEml.php create mode 100644 languages/messages/MessagesFf.php create mode 100644 languages/messages/MessagesFj.php create mode 100644 languages/messages/MessagesGag.php create mode 100644 languages/messages/MessagesGan.php create mode 100644 languages/messages/MessagesGd.php create mode 100644 languages/messages/MessagesGlk.php create mode 100644 languages/messages/MessagesGot.php create mode 100644 languages/messages/MessagesGrc.php create mode 100644 languages/messages/MessagesHaw.php create mode 100644 languages/messages/MessagesIe.php create mode 100644 languages/messages/MessagesIg.php create mode 100644 languages/messages/MessagesIk.php create mode 100644 languages/messages/MessagesIke_cans.php create mode 100644 languages/messages/MessagesIke_latn.php create mode 100644 languages/messages/MessagesIlo.php create mode 100644 languages/messages/MessagesInh.php create mode 100644 languages/messages/MessagesIo.php create mode 100644 languages/messages/MessagesIu.php create mode 100644 languages/messages/MessagesJut.php create mode 100644 languages/messages/MessagesKk_arab.php create mode 100644 languages/messages/MessagesKk_cyrl.php create mode 100644 languages/messages/MessagesKk_latn.php create mode 100644 languages/messages/MessagesKl.php create mode 100644 languages/messages/MessagesKw.php create mode 100644 languages/messages/MessagesKy.php create mode 100644 languages/messages/MessagesLad.php create mode 100644 languages/messages/MessagesLb.php create mode 100644 languages/messages/MessagesLbe.php create mode 100644 languages/messages/MessagesLfn.php create mode 100644 languages/messages/MessagesLij.php create mode 100644 languages/messages/MessagesLld.php create mode 100644 languages/messages/MessagesLmo.php create mode 100644 languages/messages/MessagesLoz.php create mode 100644 languages/messages/MessagesMai.php create mode 100644 languages/messages/MessagesMdf.php create mode 100644 languages/messages/MessagesMg.php create mode 100644 languages/messages/MessagesMo.php create mode 100644 languages/messages/MessagesMyv.php create mode 100644 languages/messages/MessagesNa.php create mode 100644 languages/messages/MessagesNan.php create mode 100644 languages/messages/MessagesNb.php create mode 100644 languages/messages/MessagesNov.php create mode 100644 languages/messages/MessagesNso.php create mode 100644 languages/messages/MessagesNy.php create mode 100644 languages/messages/MessagesPag.php create mode 100644 languages/messages/MessagesPam.php create mode 100644 languages/messages/MessagesPap.php create mode 100644 languages/messages/MessagesPdc.php create mode 100644 languages/messages/MessagesPfl.php create mode 100644 languages/messages/MessagesPih.php create mode 100644 languages/messages/MessagesPnt.php create mode 100644 languages/messages/MessagesRm.php create mode 100644 languages/messages/MessagesRuq.php create mode 100644 languages/messages/MessagesRuq_cyrl.php create mode 100644 languages/messages/MessagesRuq_grek.php create mode 100644 languages/messages/MessagesRuq_latn.php create mode 100644 languages/messages/MessagesSco.php create mode 100644 languages/messages/MessagesSdc.php create mode 100644 languages/messages/MessagesSei.php create mode 100644 languages/messages/MessagesSg.php create mode 100644 languages/messages/MessagesShi.php create mode 100644 languages/messages/MessagesSi.php create mode 100644 languages/messages/MessagesSimple.php create mode 100644 languages/messages/MessagesSm.php create mode 100644 languages/messages/MessagesSma.php create mode 100644 languages/messages/MessagesSrn.php create mode 100644 languages/messages/MessagesSs.php create mode 100644 languages/messages/MessagesSt.php create mode 100644 languages/messages/MessagesStq.php create mode 100644 languages/messages/MessagesSw.php create mode 100644 languages/messages/MessagesTet.php create mode 100644 languages/messages/MessagesTk.php create mode 100644 languages/messages/MessagesTl.php create mode 100644 languages/messages/MessagesTo.php create mode 100644 languages/messages/MessagesTokipona.php create mode 100644 languages/messages/MessagesTp.php create mode 100644 languages/messages/MessagesVe.php create mode 100644 languages/messages/MessagesWuu.php create mode 100644 languages/messages/MessagesXh.php create mode 100644 languages/messages/MessagesXmf.php create mode 100644 languages/messages/MessagesYdd.php create mode 100644 languages/messages/MessagesYo.php create mode 100644 languages/messages/MessagesYue.php create mode 100644 languages/messages/MessagesZh_hans.php create mode 100644 languages/messages/MessagesZh_hant.php create mode 100644 languages/messages/MessagesZh_min_nan.php create mode 100644 languages/messages/MessagesZu.php create mode 100644 maintenance/archives/patch-image_reditects.sql create mode 100644 maintenance/archives/patch-protected_titles.sql create mode 100644 maintenance/fetchText.php create mode 100644 maintenance/language/StatOutputs.php create mode 100644 maintenance/postgres/archives/patch-protected_titles.sql create mode 100644 maintenance/postgres/archives/patch-ts2pagetitle.sql create mode 100644 maintenance/preprocessorFuzzTest.php create mode 100644 maintenance/rebuildmessages.php create mode 100644 maintenance/testRunner.postgres.sql create mode 100644 opensearch_desc.php5 create mode 100644 skins/Modern.deps.php create mode 100644 skins/Modern.php create mode 100644 skins/archlinux/headbg.jpg create mode 100644 skins/archlinux/wiki-indexed.png create mode 100644 skins/archlinux/wiki.png create mode 100644 skins/modern/audio.png create mode 100644 skins/modern/bullet.gif create mode 100644 skins/modern/discussionitem_icon.gif create mode 100644 skins/modern/document.png create mode 100644 skins/modern/external.png create mode 100644 skins/modern/file_icon.gif create mode 100644 skins/modern/footer-grad.png create mode 100644 skins/modern/link_icon.gif create mode 100644 skins/modern/lock_icon.gif create mode 100644 skins/modern/mail_icon.gif create mode 100644 skins/modern/main.css create mode 100644 skins/modern/news_icon.png create mode 100644 skins/modern/print.css create mode 100644 skins/modern/rtl.css create mode 100644 skins/modern/video.png create mode 100644 skins/simple/rtl.css diff --git a/HISTORY b/HISTORY index f98333a7..e95ca184 100644 --- a/HISTORY +++ b/HISTORY @@ -1,5 +1,593 @@ Change notes from older releases. For current info see RELEASE-NOTES. +== MediaWiki 1.11 == + +This is the Summer 2007 branch release of MediaWiki. + +MediaWiki is now using a "continuous integration" development model with +quarterly snapshot releases. The latest development code is always kept +"ready to run", and in fact runs our own sites on Wikipedia. + +Release branches will continue to receive security updates for about a year +from first release, but nonessential bugfixes and feature developments +will be made on the development trunk and appear in the next quarterly release. + +Those wishing to use the latest code instead of a branch release can obtain +it from source control: http://www.mediawiki.org/wiki/Download_from_SVN + +== Configuration changes since 1.10 == + +* $wgThumbUpright - Adjust width of upright images when parameter 'upright' is + used +* $wgAddGroups, $wgRemoveGroups - Finer control over who can assign which + usergroups +* $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites +* $wgShowHostnames - Expose server host names through the API and HTML comments +* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally + +== New features since 1.10 == + +* (bug 8868) Separate "blocked" message for autoblocks +* Adding expiry of block to block messages +* Links to redirect pages in categories are wrapped in + +* Introduced 'ImageOpenShowImageInlineBefore' hook; see docs/hooks.txt for + more information +* (bug 9628) Show warnings about slave lag on Special:Contributions, + Special:Watchlist +* (bug 8818) Expose "wpDestFile" as parameter $1 to "uploaddisabledtext" +* Introducing new image keyword 'upright' and corresponding variable + $wgThumbUpright. This allows better proportional view of upright images + related to landscape images on a page without nailing the width of upright + images to a fix value which makes views for anon unproportional and user + preferences useless +* (bug 6072) Introducing 'border' keyword to the [[Image:]] syntax +* Introducing 'frameless' keyword to [[Image:]] syntax which respects the + user preferences for image width like 'thumb' but without a frame. +* (bug 7960) Link to "what links here" for each "what links here" entry +* Added support for configuration of an arbitrary number of commons-style + file repositories. +* Added a Content-Disposition header to thumb.php output +* Improved thumb.php error handling +* Display file history on local image description pages of shared images +* Added $wgArticleRobotPolicies +* (bug 10076) Additional parameter $7 added to MediaWiki:Blockedtext + containing, the ip, ip range, or username whose block is affecting the +* (bug 7691) Show relevant lines from the deletion log when re-creating a + previously deleted article +* Added variables 'wgRestrictionEdit' and 'wgRestrictionMove' for JS to header +* (bug 9898) Allow viewing all namespaces in Special:Newpages +* (bug 10139) Introduce 'EditSectionLink' and 'EditSectionLinkForOther' hooks; + see docs/hooks.txt for details +* (bug 9769) Provide "watch this page" toggle on protection form +* (bug 9886) Provide clear example "stub link" in Special:Preferences +* (bug 10055) Populate email address and real name properties of User objects + passed to the 'AbortNewAccount' hook +* Show result of Special:Booksources in wiki content language always, it's + normally better maintained than the generic list from the standard message + files +* (bug 7997) Allow users to be blocked from using Special:Emailuser +* (bug 8989) Blacklist 'mhtml' and 'mht' files from upload +* (bug 8760) Allow wiki links in "protectexpiry" message +* (bug 5908) Add "DEFAULTSORTKEY" and "DEFAULTCATEGORYSORT" aliases for + "DEFAULTSORT" magic word +* (bug 10181) Support the XCache object caching mechanism +* (bug 9058) Introduce '--aconf' option for all maintenance scripts, to provide + a path to the AdminSettings.php file +* (bug 8781) Remind users to check file permissions for LocalSettings.php + post-installation +* Use shared.css for all skins and oldshared.css in place of common.css for + pre-Monobook skins. As always, modifications should go in-wiki to MediaWiki: + Common.css and MediaWiki:Monobook.css. +* (bug 8869) Introduce Special:Uncategorizedtemplates +* (bug 8734) Different log message when article protection level is changed +* (bug 8458, 10338) Limit custom signature length to $wgMaxSigChars Unicode + characters +* (bug 10096) Added an ability to query interwiki map table +* On reupload, add a null revision to the image description page +* Group log output by date +* Kurdish interface latin/arabic writing system with transliteration +* Support wiki text in all query page headers +* Add 'Orphanedpages' as an alias to Special:Lonelypages +* (bug 9328) Use "revision-info-current" message in place of "revision-info" + when viewing the current revision of a page, if available +* (bug 8890) Enable wiki text for "license" message +* Throw a showstopper exception when a hook function fails to return a value. + Forgetting to give a 'true' return value is a very common error which tends + to cause hard-to-track-down interactions between extensions. +* Use $wgJobClasses to determine the correct Job to instantiate for a particular + queued task; allows extensions to introduce custom jobs +* (bug 10326) AJAX-based page watching and unwatching has been cleaned up and + enabled by default. +* Added option to install to MyISAM +* (bug 9250) Remove hardcoded minimum image name length of three characters +* Fixed DISPLAYTITLE behaviour to reject titles which don't normalise to the + same title as the current page, and enabled per default +* Wrap site CSS and JavaScript in a
 tag, like user JS/CSS
+* (bug 10196) Add classes and dir="ltr" to the 
s on CSS and JS pages (new
+  classes: mw-code, mw-css, mw-js)
+* (bug 6711) Add $wgAddGroups and $wgRemoveGroups to allow finer control over
+  usergroup assignment.
+* Introduce 'UserEffectiveGroups' hook; see docs/hooks.txt for more information
+* (bug 10387) Detect and handle '.php5' extension environments at install time
+* Introduce 'ShowRawCssJs' hook; see docs/hooks.txt for more information
+* (bug 10404) Show rights log for the selected user in Special:Userrights
+* New javascript for upload page that will show a warning if a file with the
+  "destination filename" already exists.
+* Add 'editsection-brackets' message to allow localization (or removal) of the
+  brackets in the "[edit]" link for sections
+* (bug 10437) Move texvc styling to shared.css
+* Introduce "raw editing" mode for the watchlist, to allow bulk additions,
+  removals, and convenient exporting of watchlist contents
+* Show "undo" links in page histories
+* Option to jump to specified time period in user contributions
+* Improved feedback on "rollback success" page
+* Show distinct 'namespaceprotected' message to users when namespace protection
+  prevents page editing
+* (bug 9936) Per-edit suppression of preview-on-first edit with "preview=no"
+* Allow showing a one-off preview on first edit with "preview=yes"
+* (bug 9151) Remove timed redirects on "Return to X" pages for accessibility.
+* Link to user logs in toolbox when viewing a user page
+* (bug 10508) Allow HTML attributes on 
+* (bug 1962) Allow HTML attributes on 
+* (bug 10530) Introduce optional "sp-contributions-explain" message for
+  additional explanation in Special:Contributions
+* (bug 10520) Preview licences during upload via AJAX (toggle with
+  $wgAjaxLicensePreview)
+* New Parser::setTransparentTagHook for parser extension and template
+  compatibility
+* Introduced 'ContributionsToolLinks' hook; see docs/hooks.txt for more
+  information
+* Add a message if category is empty
+* Add CSS compatibility for Opera 9.5
+* Remove largely untested handheld stylesheet, which was causing more trouble
+  than good.  Proper handheld support will be added at a future date.  For now,
+  display should be acceptable either with CSS turned off or when using a so-
+  phisticated handheld browser.
+* (bug 3173) Option to offer exported pages as a download, rather than displaying
+  inline, as in most browsers
+* Pass the user as an argument to 'isValidPassword' hook callbacks; see
+  docs/hooks.txt for more information
+* Introduce 'UserGetRights' hook; see docs/hooks.txt for more information
+* (bug 9595) Pass new Revision to the 'ArticleInsertComplete' and
+  'ArticleSaveComplete' hooks; see docs/hooks.txt for more information
+* (bug 9575) Accept upload description from GET parameters
+* Skip the difference engine cache when 'action=purge' is used while requesting
+  a difference page, to allow refreshing the cache in case of errors
+* (bug 10701) Link to Special:Listusers in default Special:Statistics messages
+* Improved file history presentation
+* (bug 10739) Users can now enter comments when reverting files
+* Improved handling of permissions errors
+* (bug 10793) "Mark patrolled" links will now be shown for users with
+  patrol permissions on all eligible diff pages
+* (bug 10655) Show standard tool links for blocked users in block log messages
+* Show standard tool links for blocked users in Special:Ipblocklist
+* Miscellaneous aesthetic improvements to Special:Ipblocklist
+* (bug 10826) Added link trail with Cyrillic characters for Mongolian language
+* (bug 10859) Introduce 'UserGetImplicitGroups' hook; see docs/hooks.txt for
+  more information
+* (bug 10832) Include user information when viewing a deleted revision
+* (bug 10872) Fall back to sane defaults when generating protection selector
+  labels for custom restriction levels
+* Show edit count in user preferences
+* Improved support for audio/video extensions
+* (bug 10937) Distinguish overwritten files in upload log
+* Introduce 'ArticleUpdateBeforeRedirect' hook; see docs/hooks.txt for more
+  information
+* Confirmation is now required when deleting old versions of files
+* (bug 7535) Users can now enter comments when deleting old versions of files
+* (bug 11001) Submit Special:Newpages as a GET, rather than a POST request
+* The  around links to watched pages in change lists now
+  has a class - "mw-watched"
+* (bug 9002) Provide a "view/restore deleted edits" link on Special:Upload
+  when a destination filename is provided that corresponds with previous
+  deleted files
+* Make the "invalid special page" message clearer
+* Add accesskey 's' and tooltip to 'upload file' button at Special:Upload
+* Introduced 'SkinAfterBottomScripts' hook; see docs/hooks.txt for
+  more information
+* (bug 11095) Honour "preview on first edit" preference when preloading
+  text for a non-existent page
+* (bug 11022) Use a more accurate page title for Special:Whatlinkshere and
+  Special:Recentchangeslinked
+* Add link to user contributions in normal watchlist edit mode
+* (bug 9426) Add 'newsectionheaderdefaultlevel' message to allow 
+  modification of the heading formatting for new sections when section=new 
+  argument is supplied
+* (bug 10836) Add 'newsectionsummary' message to allow modification of the 
+  text that prefixes a new section link in Recent Changes
+  
+== Bugfixes since 1.10 ==
+
+* (bug 9712) Use Arabic comma in date/time formats for Arabic and Farsi
+* (bug 9670) Follow redirects when render edit section links to transcluded
+  templates.
+* (bug 6204) Fix incorrect unindentation with $wgMaxTocLevel
+* (bug 3431) Suppress "next page" link in Special:Search at end of results
+* Don't show unblock form if the user doesn't have permission to use it
+  (cosmetic change, no vulnerabilities existed)
+* Subtitle success message when unblocking a block ID instead of a pseudo link
+  like [[User:#123|#123]]
+* Use the standard HTTP fetch functions when retrieving remote wiki pages
+  through transwiki, so we can take advantage of cURL goodies if available
+* Disable user JavaScript on Special:Userlogin, Special:Resetpass and
+  Special:Preferences, to avoid a compromised script sniffing passwords, etc.
+* (bug 9854, 3770) Clip overflow text in gallery boxes for visual cleanliness
+  instead of letting it flow outside the box or trigger ugly scroll bars.
+* Tooltips for print version and permalink
+* Links to the MediaWiki namespace for system messages having their default
+  values are no longer shown as nonexistent (e.g., in red)
+* Special:Ipblocklist differentiates between empty list and no search results.
+* (bug 5375) profiling does not respect read-only mode.
+* (bug 7070) monobook/user.gif has antialias artifacts
+* (bug 9123) Safer way when applying $wgLocalTZoffset
+* (bug 9896) Documentation for $wgSquidServers and X-FORWARDED-FOR
+* (bug 9417) Uploading new versions of images when using Postgres no longer 
+  throws warnings.
+* (bug 9908) Using tsearch2 with Postgres 8.1 no longer gives an error.
+* (bug 1438) Fix for diff table layout on very wide lines.
+  Diff style rules have been broken out to common/diff.css,
+  and the dupes removed from the default skin files.
+  Skins can still override the default rules.
+* (bug 1229) Balance columns in diff display evenly
+* Right-align diff line numbers in RTL language display
+* (bug 9332) Fix instructions in tests/README
+* (bug 9813) Reject usernames containing '#' to avoid silent truncation
+  of fragments during the normalisation process
+* (bug 7989) RSS feeds content now use black text when using white background.
+* (bug 9971) Typo in a french language message.
+* (bug 9973) Changed size was shown in advanced recentchanges collapsible items
+  with $wgRCShowChangedSized = false.
+* Fix PHP strict standards warning in enhanced recent changes.
+* (bug 5850) Added hexadecimal html entities comments for $digitTransformTable
+  entries.
+* (bug 7432) Change language name for Aromanian (roa-rup)
+* (bug 908) Unexistent special pages now generate a red link.
+* (bug 7899) Added \hline and \vline to the list of allowed TeX commands
+* (bug 7993) support mathematical symbol classes
+* (bug 10007) Allow Block IP to work with Postgrs again.
+* Add Google Wireless Transcoder to the Unicode editing blacklist
+* (bug 10083) Fix for Special:Version breakage on PHP 5.2 with some hooks
+* (bug 3624) TeX: \ker, \hom, \arg, \dim treated like \sin & \cos
+* (bug 10132, 10134) Restore back-compatibility Image::imageUrl() function
+* (bug 10113) Fix double-click for view source on protected pages
+* (bug 10117) Special:Wantedpages doesn't handle invalid titles in result
+  set [now prints out a warning]
+* (bug 10118) Introduced Special:Mostlinkedtemplates, report which lists
+  templates with a high number of inclusion links
+* (bug 10104) Fixed Database::getLag() for PostgreSQL and Oracle
+* (bug 9820) session.save_path check no longer halts installation, but
+  warns of possible bad values
+* (bug 9978) Fixed session.save_path validation when using extended
+  configuration format, e.g. "5;/tmp"
+* Don't generate a diff link in the patrol log if the page doesn't exist
+* (bug 10067) Translations for former skins removed from message files
+* (bug 9993) Force $wgShowExceptionDetails on during installation
+* (bug 9980) Validate administrator username and password during
+  installation
+* (bug 9383) Don't set a default value for BLOB column in rc-deleted
+  database patch
+* (bug 10149) Don't show full template list on section-0 edit
+* (bug 9909) Ensure access to binary fields in the math table use encodeBlob() 
+  and decodeBlob()
+* (bug 6743) Don't link broken image links to the upload form when uploads
+  are disabled
+* (bug 9679) Improve documentation for $wgSiteNotice
+* (bug 10215) Show custom editing introduction when editing existing pages
+* (bug 10223) Fix edit link in noarticletext localizations for fr, oc
+* (bug 10247) Fix IP address regex to avoid false positive IPv6 matches
+* (bug 9948) Workaround for diff regression with old Mozilla versions
+* (bug 10265) Fix regression in category image gallery paging
+* (bug 8577) Fix some weird misapplications of time zones.
+  {{CURRENT*}} functions now consistently use UTC as intended, while
+  {{LOCAL*}} functions return local time per server config or $wgLocaltimezone.
+  Signature dates for Japanese and other languages including weekday now show
+  the correct day to match the rest of the time in local time.
+* Escape the output of magic variables that return page name or part of it
+* (bug 10309) Initialise parser state properly in extractSections(), fixes
+  some cases where section edits broke because tags were improperly stripped
+* Avoid PHP notice errors when doing HTTP proxy purges for an empty list
+* As intended, *skip* the HTTP proxy purges when doing HTCP purges
+* (bug 9696) Fix handling of brace transformations in "pagemovedtext"
+* (bug 10325) Fix regression in form action on Special:Listusers
+* Fixed installation on MyISAM or old InnoDB with charset=utf8, was giving 
+  overlong key errors.
+* Fixed zero-padding issues with MySQL 5 binary schema
+* (bug 10344) Don't follow a redirect after changing its protection level
+* (bug 10333) Correct date format in Slovenian
+* (bug 10160) Show error message for unknown namespace on Special:Allpages and
+  Special:Prefixindex; making forms prettier for RTL wikis.
+* (bug 10334) Replace normal spaces before percent (%) signs with non-breaking
+  spaces
+* (bug 10372) namespaceDupes.php no longer ignores namespace aliases
+* (bug 10198) namespaceDupes.php no longer ignores interwiki prefixes
+* namespaceDupes.php should work better for initial-lowercase wikis
+* (bug 10377) "Permanent links" to revisions still work if the page is moved
+  and the redirect deleted
+* (bug 7071) Properly handle an 'oldid' passed to view or edit that doesn't
+  match the given title. Fixes inconsistencies with talk, history, edit links.
+* (bug 10397) Fix AJAX watch error fallback when we receive a bogus result
+* (bug 10396) Fix AJAX error when $wgScriptPath/index.php is not valid;
+  using $wgScript now included in JS info
+* Use native XMLHttpRequest class in preference to ActiveX on IE 7; this
+  avoids the "ActiveX "Do you want to allow ActiveX?" prompt when something
+  security settings are cranked this way and AJAX-y gets used.
+* Delay AJAX watch initialization until click so IE 6 with ugly security
+  settings doesn't prompt you until you use the link.
+* (bug 10401) Provide non-redirecting link to original title in Special:Movepage
+* Fix broken handling of log views for page titles consisting of one
+  or more zeros, e.g. "0", "00" etc.
+* Fix read permission check for special pages with subpage parameters, e.g.
+  Special:Confirmemail
+* Fix read permission check for unreadable page titles which are numerically
+  equivalent to a whitelisted title
+* '?>' closing tag removed from all files to help avoid problems with extraneous
+  whitespace (broken XML feeds, etc.)
+* Don't use garbled parser cache output when viewing custom CSS or JavaScript
+  pages
+* (bug 10406) Fix Special:Listusers filter form for non-ASCII localizations
+* Fix empty message checks for message names containing &
+  This corrects some odd behavior with sidebar items and custom namespaces
+  containing ampersands.
+* (bug 10375) Change thousands separator character to   for Latin (la)
+* (bug 10477) Fix AJAX watch for Farsi on Firefox: JavaScript encoding tweak
+* (bug 10496) Fix broken DISTINCT option logic in database backend
+* Fix CSS media declaration for "screen, projection"; was causing some
+  validation issues
+* (bug 10495) $wgMemcachedDebug set twice in includes/DefaultSettings.php
+* (bug 10316) Prevent inconsistent cached skin settings in gen=js by setting
+  the intended skin directly in the URL.
+* (bug 9903) Don't mark redirects in categories as stubs
+* (bug 6965) Cannot include "Template:R" with {{R}} (magic word conflict)
+* Padding parser functions now work with strings like '0' that evaluate to false
+* (bug 10332) Title->userCan( 'edit' ) may return false positive
+* Fix bug with  in front of links for wikis where linkPrefixExtension is true
+* (bug 10552) Suppress rollback link in history for single-revision pages
+* (bug 10538) Gracefully handle invalid input on move success page
+* Fix for Esperanto double-x-encoding in move success page
+* (bug 10526) Fix toolbar/insertTags behavior for IE 6/7 and Opera (8+)
+  Now matches the selection behavior on Mozilla / Safari.
+  Patch by Alex Smotrov.
+* Don't show non-functional toolbar buttons on Opera 7 anymore
+* (bug 9151) Fix relative subpage links with section fragments
+* (bug 10560) Adding a space between category letter heading and "continues"
+* (bug 4650) Keep impossibly large/small counts off Special:Statistics
+* (bug 10608) PHP notice when installing with PostgreSQL
+* (bug 10615) Fix for transwiki import when CURL not available
+* (bug 8054) Return search page for empty search requests with ugly URLs
+* (bug 10572) Force refresh after clearing visitation timestamps on watchlist
+* (bug 10631) Warn when illegal characters are removed from filename at upload
+* Fix several JavaScript bugs under MSIE 5/Macintosh
+* (bug 10591) Use Arabic numerals (0,1,2...) for the Malayam language
+* (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0
+* Work around Safari bug with pages ending in ".gz" or ".tgz"
+* Removed obsolete maintenance/changeuser.sql script; use RenameUser extension
+* (bug 2735) "Preview" shown in title bar for action=submit on special pages
+* Removed "restore" links from the deletion log embedded in Special:Undelete
+* Improved error reporting and robustness for file delete/undelete.
+* Improved speed of file delete by storing the SHA-1 hash in image/oldimage
+* Fixed leading zero in base 36 SHA-1 hash
+* Protection form no longer produces JavaScript errors
+* (bug 10741) File histories show "delete" links for non-sysops
+* (bug 10744) Treat "noarticletext" and "noarticletextanon" as wiki text when
+  used on a non-existent page with "action=info"
+* Fix escaping of raw message text when used on a non-existent page with
+  "action=info"
+* (bug 10683) Fix inconsistent handling of URL-encoded titles in links
+  used in redirects (i.e. they now work)
+* (bug 8878) Changes to $dateFormats in German localization (removing unused,
+  nonexistent formats, putting time after date)
+* (bug 10769) Database::update() should return boolean result
+* Fix preference checkbox display for right-to-left languages which caused
+  them to be hidden in IE in some cases
+* Fix upload form display in right-to-left languages
+* Fixed regression in blocking of username '0'
+* (bug 9437) Don't overwrite edit form submission handler when setting up
+  edit box scroll position preserve/restore behaviour
+* (bug 10805) Fix "undo" link when viewing the diff of the most recent
+  change to a page using "diff=0"
+* (bug 10765) img_auth.php will now refuse logged-out requests where
+  $wgWhitelistRead is undefined, instead of (incorrectly) honouring them
+* Fixed img_auth.php file name extraction for whitelist checking
+* Tweak spacing of email preference display
+* Table sorting JavaScript prefers textContent over innerText to allow hidden
+  sort keys to work on Safari
+* (bug 4530) Fix local name of Kurdish language
+* (bug 10830) Fix local name of Haitian Creole language
+* Fix invalid XHTML in Special:Protectedpages
+* Fix comments in contributions and log pages for right-to-left languages
+* Make installer include_path-independent, so it should work on hosts which
+  disable user setting of PHP include_path setting
+* glob() is horribly unreliable and doesn't work on some systems, including
+  free.fr shared hosting. No longer using it in Language::getLanguageNames()
+* (bug 10763) Fix multi-insert logic for PostgreSQL
+* Fix invalid XHTML when viewing a deleted revision
+* Fix syntax error in translations of magic words in Romanian language
+* (bug 8737) Fix warnings caused by incorrect use of `/dev/null` when piping
+  process error output under Windows
+* (bug 7890) Don't list redirects to special pages in Special:BrokenRedirects
+* (bug 10783) Resizing PNG-24 images with GD no longer causes all alpha
+  channel transparency to be lost and transparent pixels to be turned black
+* (bug 9339) General error pages were transforming messages and their parameters
+  in the wrong order
+* (bug 9026) Incorrect heading numbering when viewing Special:Statistics with
+  "auto-numbered headings" enabled
+* Fixed invalid XHTML in Special:Upload
+* (bug 11013) Make sure dl() is available before attempting to use it to check
+  available databases in installer
+* Resizing transparent GIF images with GD now retains transparency by skipping
+  resampling
+* (bug 11065) Fix regression in handling of wiki-formatted EXIF metadata
+* Double encoding broke Special:Newpages for some languages
+* Adding a newline before the statistics footer, to prevent parsing problems
+* Preventing the TOC from appearing in Special:Statistics
+* (bug 11082) Fix check for fully-specced table names in Database::tableName
+* (bug 11067) Fix regression in upload conflict thumbnail display
+* (bug 10985) Resolved cached entries on Special:DoubleRedirects were being
+  supressed, breaking paging - now strikes out "fixed" results
+* (bug 8393)  and  need to be preserved (without attributes) for
+  entries in the table of contents
+* (bug 11114) Fix regression in read-only mode error display during editing
+* Force non-MySQL databases to use an ORDER BY in SpecialAllpages to ensure 
+  that the first page_title is truly the first page title.
+* (bug 10836) Change the summary on creating of new section
+* Inclusion of Special:Wantedpages now works again
+
+== API changes since 1.10 ==
+
+Full API documentation is available at http://www.mediawiki.org/wiki/API
+
+* New properties: links, templates, images, langlinks, categories, external
+  links
+* Breaking Change: imagelinks renamed into imageusage (il->iu)
+* Bug fix: incorrect generator behavior in some cases
+* JSON format allows an optional callback function to wrap the result.
+* Login module disabled until a more secure solution can be implemented
+* (bug 9938) Querying by revision identifier returns the most recent revision
+  for the corresponding page, rather than the requested revision
+* (bug 8772) Filter page revision queries by user
+* (bug 9927) User contributions queries do not accept IP addresses
+* Watchlist feed now reports a proper feed item when the user is not logged in
+* Watchlist feed date bug fixed - automatically shows one last day
+* Watchlist feed now allows to specify number of hours to monitor
+* list=allpages now returns a list instead of a map in JSON format
+* Breaking Change: in json, revisions are now returned as a list, not as a map.
+* Add: prop=info can show page is new flag, current page length, and visit
+  counter.
+* Change: Query watchlist now shows flags only when explicitly requested with
+  wlparam=flags
+* rc_this_oldid (textid) is no longer accessible from query watchlist
+* action=usercontribs: additional filtering by ucshow=; selection of needed
+  fields with ucprop=; the textid (rev_text_id) is no longer being exposed
+* (bug 9970) Breaking Change: backlinks, embeddedin and imageusage now return
+  lists in JSON instead of a map, and do not return anything when titles do
+  not exist
+* (bug 9121) Introduced indexpageids query parameter to list the page_id
+  values of all returned page items
+* (bug 10147) Now interwiki titles are not processed but added to a separate
+  "interwiki" section of the output.
+* Added categorymembers list to query for pages in a category.
+* (bug 10260) Show page protection status
+* (bug 10392) Include MediaWiki version details in version output
+* (bug 10411) Site language in meta=siteinfo
+* (bug 10391) action=help doesn't return help if format is fancy markup
+* backlinks, embeddedin and imageusage lists should use (bl|ei|iu)title parameter
+  instead of titles. Titles for these lists is obsolete and might stop working soon.
+* Added prop=imageinfo - gets image properties and upload history
+* (bug 10211) Added db server replication lag information in meta=siteinfo
+* Added external url search within wiki pages (list=exturlusage)
+* Added link enumeration (list=alllinks)
+* Added registered users enumeration (list=allusers)
+* Added full text search in titles and content (list=search)
+* (bug 10684) Expanded list=allusers functionality
+* Possible breaking change: prop=revisions no longer includes pageid for rvprop=ids
+* Added rvprop=size to prop=revisions (The size will not be shown if it is NULL in the database)
+* list=allpages now allows to filter by article min/max size and protection status
+* Added site statistics (siprop=statistics for meta=siteinfo)
+* (bug 10902) Unable to fetch user contributions from IP addresses
+* `list=usercontribs` no longer requires that the user exist
+* (bug 10971) `aufrom` parameter doesn't work with spaces
+* Fix username handling issue with `auprefix` parameter
+* Treat underscores as spaces for `aufrom` and `auprefix` parameters
+* Added edit/delete/... token retrieval to prop=info
+* Added meta=userinfo - logged-in user information, group membership, rights
+* (bug 11072) Fix regression in API image history query
+* (bug 11115) Adding SHA1 hash to imageinfo query
+* (bug 10898) API does not return an edit token for non-existent pages
+* (bug 10890) Timestamp support for categorymembers query
+* (bug 10980) Add exclude redirects on backlinks
+* IPv6 titles in User namespace are normalized (run cleanupTitles.php to fix any old stray pages)
+
+== Maintenance script changes since 1.10 ==
+
+* Add support for wgMaxTocLevel option in parserTests
+* (bug 6823) Disable article view counter in maintenance/dumpHTML.php
+* Fix maintenance/importImages.php so it doesn't barf PHP errors when no
+  suitable files are found, and make the list of extensions an option (defaults
+  to $wgFileExtensions)
+* Add option to maintenance/createAndPromote.php to give the user bureaucrat
+  permissions (--bureaucrat)
+* Allow overwriting existing files with a conflicting name using
+  maintenance/importImages.php
+* (bug 10266) Use native newlines when rebuilding a messages file.
+
+== Languages updated since 1.10 ==
+
+* Afrikaans (af)
+* Arabic (ar)
+* Bikol (bcl)
+* Bulgarian (bg)
+* Catalan (ca)
+* Danish (da)
+* German (de)
+* Greek (el)
+* Esperanto (eo)
+* Spanish (es)
+* Estonian (et)
+* Extremaduran (ext)
+* Farsi (fa)
+* Finnish (fi)
+* Vöro (fiu-vro)
+* French (fr)
+* Français Cadien (frc) (new)
+* Franco-Provençal/Arpetan (frp)
+* Galician (gl)
+* Hakka (hak)
+* Hebrew (he)
+* Upper Sorbian (hsb)
+* Haitian (ht)
+* Indonesian (id)
+* Icelandic (is)
+* Italian (it)
+* Japanese (ja)
+* Georgian (ka)
+* Kabyle (kab)
+* Kazakh (kk)
+* Korean (ko)
+* Kinaray-a (krj) (new)
+* Kurdish (ku)
+* Latin (la)
+* Lao (lo)
+* Lithuanian (lt)
+* Latviešu (lv)
+* Malayalam (ml)
+* Bahasa Melayu (ms)
+* Burmese (my)
+* Low German (nds)
+* Dutch (nl)
+* Norwegian (no)
+* Occitan (oc)
+* Punjabi (Gurmukhi) (pa)
+* Polish (pl)
+* Piedmontese (pms)
+* Portuguese (pt)
+* Romani (rmy)
+* Romanian (ro)
+* Aromanian (roa-rup)
+* Russian (ru)
+* Sakha (sah)
+* Sango (se) (new)
+* Slovak (sk)
+* Slovenian (sl)
+* Shona (sn)
+* Somali (so)
+* Albanian (sq)
+* Sundanese (su)
+* Swedish (sv)
+* Tamil (ta)
+* Thai (th)
+* Tigrinya (ti)
+* Setswana (tn)
+* Tok Pisin (tpi)
+* Uyghur (ug)
+* Volapük (vo)
+* Winaray (war) (new)
+* Yiddish (yi)
+* Old Chinese / Late Middle Chinese (zh-classical)
+* Chinese (PRC) (zh-cn)
+* Chinese (Taiwan) (zh-tw)
+* Cantonese (zh-yue)
+
 == MediaWiki 1.10 ==
 
 This is the Spring 2007 branch release of MediaWiki.
@@ -4501,10 +5089,10 @@ For notes on 1.3.x and older releases, see HISTORY.
 === Online documentation ===
 
 Documentation for both end-users and site administrators is currently being
-built up on Meta-Wikipedia, and is covered under the GNU Free Documentation
+built up on MediaWiki.org, and is covered under the GNU Free Documentation
 License:
 
-  http://meta.wikipedia.org/wiki/Help:Contents
+  http://www.mediawiki.org/
 
 
 === Mailing list ===
@@ -4512,10 +5100,10 @@ License:
 A MediaWiki-l mailing list has been set up distinct from the Wikipedia
 wikitech-l list:
 
-  http://mail.wikipedia.org/mailman/listinfo/mediawiki-l
+  http://lists.wikimedia.org/mailman/listinfo/mediawiki-l
 
 A low-traffic announcements-only list is also available:
-  http://mail.wikipedia.org/mailman/listinfo/mediawiki-announce
+  http://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
 
 It's highly recommended that you sign up for one of these lists if you're
 going to run a public MediaWiki, so you can be notified of security fixes.
diff --git a/README b/README
index e3a387ef..5f659df5 100644
--- a/README
+++ b/README
@@ -8,8 +8,8 @@ INSTALL, and UPGRADE.
 MediaWiki is the software used for Wikipedia [http://www.wikipedia.org/] and the
 other Wikimedia Foundation websites. Compared to other wikis, it has an
 excellent range of features and support for high-traffic websites using
-multiple servers (Wikimedia sites peak in the 5000+ requests per second range
-as of November 2005).
+multiple servers (Wikimedia sites peak in the 50000+ requests per second range
+as of January 2008).
 
 While quite usable on smaller sites, you may find you have to "roll your own"
 local documentation, and some aspects of configuration may seem overcomplicated
@@ -75,7 +75,7 @@ Please report bugs and make feature requests in our Bugzilla system:
 
 Documentation and discussion on new features may be found at:
 
-  http://www.mediawiki.org/wiki/Help:FAQ
+  http://www.mediawiki.org/wiki/Manual:FAQ
   http://www.mediawiki.org/wiki/Documentation
   http://www.mediawiki.org/wiki/Development
 
@@ -100,4 +100,4 @@ Developer discussion takes place at:
   http://lists.wikimedia.org/mailman/listinfo/wikitech-l
 
 There is also a development and support channel #mediawiki on
-irc.freenode.net, and an unoffical support forum at www.mwusers.com.
\ No newline at end of file
+irc.freenode.net, and an unoffical support forum at www.mwusers.com.
diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index 4876d79b..f38b41a5 100644
--- a/RELEASE-NOTES
+++ b/RELEASE-NOTES
@@ -3,68 +3,34 @@
 Security reminder: MediaWiki does not require PHP's register_globals
 setting since version 1.2.0. If you have it on, turn it *off* if you can.
 
-== MediaWiki 1.11.2 ==
+== MediaWiki 1.12.0 ==
 
-March 2, 2008
+This is the quarterly branch release of MediaWiki for Winter 2008.
 
-This is a security release of the Fall 2007 snapshot release of MediaWiki.
-Possible cross-site information leaks using the callback parameter for
-JSON-formatted results in the API are prevented by dropping user credentials.
-
-MediaWiki release versions prior to 1.11 are not vulnerable, as they do
-not include the callback feature which allows client-side JavaScript on
-other sites to reach API data.
-
-Changes in this release:
-
-* User credentials are dropped for API JSON requests using a callback
-* Edit tokens are not reported for API JSON requests using a callback
-
-
-== MediaWiki 1.11.1 ==
-
-January 23, 2008
-
-This is a security and bugfix release of the Fall 2007 snapshot release of
-MediaWiki. A potential XSS injection vector affecting api.php only for
-Microsoft Internet Explorer users has been closed.
-
-Changes in this release:
-
-* (bug 11450) Fix creation of objectcache table on upgrade
-* (bug 11462) Fix typo in LanguageGetSpecialPageAliases hook name
-* Fix regression in LinkBatch.php breaking PHP 5.0
-* Security fix for API on MSIE
 
+MediaWiki is now using a "continuous integration" development model with
+quarterly snapshot releases. The latest development code is always kept
+"ready to run", and in fact runs our own sites on Wikipedia.
 
-To work around the vulnerability without upgrading, you may disable the
-API if you don't need it:
+Release branches will continue to receive security updates for about a year
+from first release, but nonessential bugfixes and feature developments
+will be made on the development trunk and appear in the next quarterly release.
 
-  $wgEnableAPI = false;
+Those wishing to use the latest code instead of a branch release can obtain
+it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 
-Not vulnerable versions:
-* 1.12 or later
-* 1.11 >= 1.11.1
-* 1.10 >= 1.10.3
-* 1.9 >= 1.9.5
-* 1.8 any version (if $wgEnableAPI has been left off)
 
-Vulnerable versions:
-* 1.11 <= 1.11.0rc1
-* 1.10 <= 1.10.2
-* 1.9 <= 1.9.4
-* 1.8 any version (if $wgEnableAPI has been switched on)
+Changes since 1.12.0rc1:
 
-MediaWiki 1.7 and below are not affected as they do not include
-the API functionality, however the BotQuery extension is similarly
-vulnerable unless updated to the latest SVN version.
+* (bug 13359) Double-escaping in Special:Allpages
+* Localization updates.
 
 
-== MediaWiki 1.11.0 ==
+== MediaWiki 1.12.0rc1 ==
 
-September 10, 2007
+This is a release candidate of the Winter 2008 quarterly snapshot release
+of MediaWiki.
 
-This is the Fall 2007 snapshot release of MediaWiki.
 
 MediaWiki is now using a "continuous integration" development model with
 quarterly snapshot releases. The latest development code is always kept
@@ -77,613 +43,774 @@ will be made on the development trunk and appear in the next quarterly release.
 Those wishing to use the latest code instead of a branch release can obtain
 it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 
-== Changes since 1.11.0rc1 ==
-
-A possible HTML/XSS injection vector in the API pretty-printing mode has
-been found and fixed.
-
-The vulnerability may be worked around in an unfixed version by simply
-disabling the API interface if it is not in use, by adding this to
-LocalSettings.php:
-
-  $wgEnableAPI = false;
-
-(This is the default setting in 1.8.x.)
-
-Not vulnerable versions:
-* 1.11 >= 1.11.0
-* 1.10 >= 1.10.2
-* 1.9 >= 1.9.4
-* 1.8 >= 1.8.5
-
-Vulnerable versions:
-* 1.11 <= 1.11.0rc1
-* 1.10 <= 1.10.1
-* 1.9 <= 1.9.3
-* 1.8 <= 1.8.4 (if $wgEnableAPI has been switched on)
-
-MediaWiki 1.7 and below are not affected as they do not include
-the faulty function, however the BotQuery extension is similarly
-vulnerable unless updated to the latest SVN version.
-
-
-== Configuration changes since 1.10 ==
-
-* $wgThumbUpright - Adjust width of upright images when parameter 'upright' is
-  used
-* $wgAddGroups, $wgRemoveGroups - Finer control over who can assign which
-  usergroups
-* $wgEnotifImpersonal, $wgEnotifUseJobQ - Bulk mail options for large sites
-* $wgShowHostnames - Expose server host names through the API and HTML comments
-* $wgSaveDeletedFiles has been removed, the feature is now enabled unconditionally
-
-== New features since 1.10 ==
-
-* (bug 8868) Separate "blocked" message for autoblocks
-* Adding expiry of block to block messages
-* Links to redirect pages in categories are wrapped in
-  
-* Introduced 'ImageOpenShowImageInlineBefore' hook; see docs/hooks.txt for
-  more information
-* (bug 9628) Show warnings about slave lag on Special:Contributions,
-  Special:Watchlist
-* (bug 8818) Expose "wpDestFile" as parameter $1 to "uploaddisabledtext"
-* Introducing new image keyword 'upright' and corresponding variable
-  $wgThumbUpright. This allows better proportional view of upright images
-  related to landscape images on a page without nailing the width of upright
-  images to a fix value which makes views for anon unproportional and user
-  preferences useless
-* (bug 6072) Introducing 'border' keyword to the [[Image:]] syntax
-* Introducing 'frameless' keyword to [[Image:]] syntax which respects the
-  user preferences for image width like 'thumb' but without a frame.
-* (bug 7960) Link to "what links here" for each "what links here" entry
-* Added support for configuration of an arbitrary number of commons-style
-  file repositories.
-* Added a Content-Disposition header to thumb.php output
-* Improved thumb.php error handling
-* Display file history on local image description pages of shared images
-* Added $wgArticleRobotPolicies
-* (bug 10076) Additional parameter $7 added to MediaWiki:Blockedtext 
-  containing, the ip, ip range, or username whose block is affecting the
-* (bug 7691) Show relevant lines from the deletion log when re-creating a
-  previously deleted article
-* Added variables 'wgRestrictionEdit' and 'wgRestrictionMove' for JS to header
-* (bug 9898) Allow viewing all namespaces in Special:Newpages
-* (bug 10139) Introduce 'EditSectionLink' and 'EditSectionLinkForOther' hooks;
-  see docs/hooks.txt for details
-* (bug 9769) Provide "watch this page" toggle on protection form
-* (bug 9886) Provide clear example "stub link" in Special:Preferences
-* (bug 10055) Populate email address and real name properties of User objects
-  passed to the 'AbortNewAccount' hook
-* Show result of Special:Booksources in wiki content language always, it's
-  normally better maintained than the generic list from the standard message
-  files
-* (bug 7997) Allow users to be blocked from using Special:Emailuser
-* (bug 8989) Blacklist 'mhtml' and 'mht' files from upload
-* (bug 8760) Allow wiki links in "protectexpiry" message
-* (bug 5908) Add "DEFAULTSORTKEY" and "DEFAULTCATEGORYSORT" aliases for
-  "DEFAULTSORT" magic word
-* (bug 10181) Support the XCache object caching mechanism
-* (bug 9058) Introduce '--aconf' option for all maintenance scripts, to provide
-  a path to the AdminSettings.php file
-* (bug 8781) Remind users to check file permissions for LocalSettings.php
-  post-installation
-* Use shared.css for all skins and oldshared.css in place of common.css for
-  pre-Monobook skins.  As always, modifications should go in-wiki to MediaWiki:
-  Common.css and MediaWiki:Monobook.css.
-* (bug 8869) Introduce Special:Uncategorizedtemplates
-* (bug 8734) Different log message when article protection level is changed
-* (bug 8458, 10338) Limit custom signature length to $wgMaxSigChars Unicode
-  characters
-* (bug 10096) Added an ability to query interwiki map table
-* On reupload, add a null revision to the image description page
-* Group log output by date
-* Kurdish interface latin/arabic writing system with transliteration
-* Support wiki text in all query page headers
-* Add 'Orphanedpages' as an alias to Special:Lonelypages
-* (bug 9328) Use "revision-info-current" message in place of "revision-info"
-  when viewing the current revision of a page, if available
-* (bug 8890) Enable wiki text for "license" message
-* Throw a showstopper exception when a hook function fails to return a value.
-  Forgetting to give a 'true' return value is a very common error which tends
-  to cause hard-to-track-down interactions between extensions.
-* Use $wgJobClasses to determine the correct Job to instantiate for a particular
-  queued task; allows extensions to introduce custom jobs
-* (bug 10326) AJAX-based page watching and unwatching has been cleaned up and
-  enabled by default.
-* Added option to install to MyISAM
-* (bug 9250) Remove hardcoded minimum image name length of three characters
-* Fixed DISPLAYTITLE behaviour to reject titles which don't normalise to the
-  same title as the current page, and enabled per default
-* Wrap site CSS and JavaScript in a 
 tag, like user JS/CSS
-* (bug 10196) Add classes and dir="ltr" to the 
s on CSS and JS pages (new
-  classes: mw-code, mw-css, mw-js)
-* (bug 6711) Add $wgAddGroups and $wgRemoveGroups to allow finer control over
-  usergroup assignment.
-* Introduce 'UserEffectiveGroups' hook; see docs/hooks.txt for more information
-* (bug 10387) Detect and handle '.php5' extension environments at install time
-* Introduce 'ShowRawCssJs' hook; see docs/hooks.txt for more information
-* (bug 10404) Show rights log for the selected user in Special:Userrights
-* New javascript for upload page that will show a warning if a file with the
-  "destination filename" already exists.
-* Add 'editsection-brackets' message to allow localization (or removal) of the
-  brackets in the "[edit]" link for sections
-* (bug 10437) Move texvc styling to shared.css
-* Introduce "raw editing" mode for the watchlist, to allow bulk additions,
-  removals, and convenient exporting of watchlist contents
-* Show "undo" links in page histories
-* Option to jump to specified time period in user contributions
-* Improved feedback on "rollback success" page
-* Show distinct 'namespaceprotected' message to users when namespace protection
-  prevents page editing
-* (bug 9936) Per-edit suppression of preview-on-first edit with "preview=no"
-* Allow showing a one-off preview on first edit with "preview=yes"
-* (bug 9151) Remove timed redirects on "Return to X" pages for accessibility.
-* Link to user logs in toolbox when viewing a user page
-* (bug 10508) Allow HTML attributes on 
-* (bug 1962) Allow HTML attributes on 
-* (bug 10530) Introduce optional "sp-contributions-explain" message for
-  additional explanation in Special:Contributions
-* (bug 10520) Preview licences during upload via AJAX (toggle with
-  $wgAjaxLicensePreview)
-* New Parser::setTransparentTagHook for parser extension and template
-  compatibility
-* Introduced 'ContributionsToolLinks' hook; see docs/hooks.txt for more
-  information
-* Add a message if category is empty
-* Add CSS compatibility for Opera 9.5
-* Remove largely untested handheld stylesheet, which was causing more trouble
-  than good.  Proper handheld support will be added at a future date.  For now,
-  display should be acceptable either with CSS turned off or when using a so-
-  phisticated handheld browser.
-* (bug 3173) Option to offer exported pages as a download, rather than displaying
-  inline, as in most browsers
-* Pass the user as an argument to 'isValidPassword' hook callbacks; see
-  docs/hooks.txt for more information
-* Introduce 'UserGetRights' hook; see docs/hooks.txt for more information
-* (bug 9595) Pass new Revision to the 'ArticleInsertComplete' and
-  'ArticleSaveComplete' hooks; see docs/hooks.txt for more information
-* (bug 9575) Accept upload description from GET parameters
-* Skip the difference engine cache when 'action=purge' is used while requesting
-  a difference page, to allow refreshing the cache in case of errors
-* (bug 10701) Link to Special:Listusers in default Special:Statistics messages
-* Improved file history presentation
-* (bug 10739) Users can now enter comments when reverting files
-* Improved handling of permissions errors
-* (bug 10793) "Mark patrolled" links will now be shown for users with
-  patrol permissions on all eligible diff pages
-* (bug 10655) Show standard tool links for blocked users in block log messages
-* Show standard tool links for blocked users in Special:Ipblocklist
-* Miscellaneous aesthetic improvements to Special:Ipblocklist
-* (bug 10826) Added link trail with Cyrillic characters for Mongolian language
-* (bug 10859) Introduce 'UserGetImplicitGroups' hook; see docs/hooks.txt for
-  more information
-* (bug 10832) Include user information when viewing a deleted revision
-* (bug 10872) Fall back to sane defaults when generating protection selector
-  labels for custom restriction levels
-* Show edit count in user preferences
-* Improved support for audio/video extensions
-* (bug 10937) Distinguish overwritten files in upload log
-* Introduce 'ArticleUpdateBeforeRedirect' hook; see docs/hooks.txt for more
-  information
-* Confirmation is now required when deleting old versions of files
-* (bug 7535) Users can now enter comments when deleting old versions of files
-* (bug 11001) Submit Special:Newpages as a GET, rather than a POST request
-* The  around links to watched pages in change lists now
-  has a class - "mw-watched"
-* (bug 9002) Provide a "view/restore deleted edits" link on Special:Upload
-  when a destination filename is provided that corresponds with previous
-  deleted files
-* Make the "invalid special page" message clearer
-* Add accesskey 's' and tooltip to 'upload file' button at Special:Upload
-* Introduced 'SkinAfterBottomScripts' hook; see docs/hooks.txt for
-  more information
-* (bug 11095) Honour "preview on first edit" preference when preloading
-  text for a non-existent page
-* (bug 11022) Use a more accurate page title for Special:Whatlinkshere and
-  Special:Recentchangeslinked
-* Add link to user contributions in normal watchlist edit mode
-* (bug 9426) Add 'newsectionheaderdefaultlevel' message to allow 
-  modification of the heading formatting for new sections when section=new 
-  argument is supplied
-* (bug 10836) Add 'newsectionsummary' message to allow modification of the 
-  text that prefixes a new section link in Recent Changes
-  
-== Bugfixes since 1.10 ==
-
-* (bug 9712) Use Arabic comma in date/time formats for Arabic and Farsi
-* (bug 9670) Follow redirects when render edit section links to transcluded
-  templates.
-* (bug 6204) Fix incorrect unindentation with $wgMaxTocLevel
-* (bug 3431) Suppress "next page" link in Special:Search at end of results
-* Don't show unblock form if the user doesn't have permission to use it
-  (cosmetic change, no vulnerabilities existed)
-* Subtitle success message when unblocking a block ID instead of a pseudo link
-  like [[User:#123|#123]]
-* Use the standard HTTP fetch functions when retrieving remote wiki pages
-  through transwiki, so we can take advantage of cURL goodies if available
-* Disable user JavaScript on Special:Userlogin, Special:Resetpass and
-  Special:Preferences, to avoid a compromised script sniffing passwords, etc.
-* (bug 9854, 3770) Clip overflow text in gallery boxes for visual cleanliness
-  instead of letting it flow outside the box or trigger ugly scroll bars.
-* Tooltips for print version and permalink
-* Links to the MediaWiki namespace for system messages having their default
-  values are no longer shown as nonexistent (e.g., in red)
-* Special:Ipblocklist differentiates between empty list and no search results.
-* (bug 5375) profiling does not respect read-only mode.
-* (bug 7070) monobook/user.gif has antialias artifacts
-* (bug 9123) Safer way when applying $wgLocalTZoffset
-* (bug 9896) Documentation for $wgSquidServers and X-FORWARDED-FOR
-* (bug 9417) Uploading new versions of images when using Postgres no longer 
-  throws warnings.
-* (bug 9908) Using tsearch2 with Postgres 8.1 no longer gives an error.
-* (bug 1438) Fix for diff table layout on very wide lines.
-  Diff style rules have been broken out to common/diff.css,
-  and the dupes removed from the default skin files.
-  Skins can still override the default rules.
-* (bug 1229) Balance columns in diff display evenly
-* Right-align diff line numbers in RTL language display
-* (bug 9332) Fix instructions in tests/README
-* (bug 9813) Reject usernames containing '#' to avoid silent truncation
-  of fragments during the normalisation process
-* (bug 7989) RSS feeds content now use black text when using white background.
-* (bug 9971) Typo in a french language message.
-* (bug 9973) Changed size was shown in advanced recentchanges collapsible items
-  with $wgRCShowChangedSized = false.
-* Fix PHP strict standards warning in enhanced recent changes.
-* (bug 5850) Added hexadecimal html entities comments for $digitTransformTable
-  entries.
-* (bug 7432) Change language name for Aromanian (roa-rup)
-* (bug 908) Unexistent special pages now generate a red link.
-* (bug 7899) Added \hline and \vline to the list of allowed TeX commands
-* (bug 7993) support mathematical symbol classes
-* (bug 10007) Allow Block IP to work with Postgrs again.
-* Add Google Wireless Transcoder to the Unicode editing blacklist
-* (bug 10083) Fix for Special:Version breakage on PHP 5.2 with some hooks
-* (bug 3624) TeX: \ker, \hom, \arg, \dim treated like \sin & \cos
-* (bug 10132, 10134) Restore back-compatibility Image::imageUrl() function
-* (bug 10113) Fix double-click for view source on protected pages
-* (bug 10117) Special:Wantedpages doesn't handle invalid titles in result
-  set [now prints out a warning]
-* (bug 10118) Introduced Special:Mostlinkedtemplates, report which lists
-  templates with a high number of inclusion links
-* (bug 10104) Fixed Database::getLag() for PostgreSQL and Oracle
-* (bug 9820) session.save_path check no longer halts installation, but
-  warns of possible bad values
-* (bug 9978) Fixed session.save_path validation when using extended
-  configuration format, e.g. "5;/tmp"
-* Don't generate a diff link in the patrol log if the page doesn't exist
-* (bug 10067) Translations for former skins removed from message files
-* (bug 9993) Force $wgShowExceptionDetails on during installation
-* (bug 9980) Validate administrator username and password during
-  installation
-* (bug 9383) Don't set a default value for BLOB column in rc-deleted
-  database patch
-* (bug 10149) Don't show full template list on section-0 edit
-* (bug 9909) Ensure access to binary fields in the math table use encodeBlob() 
-  and decodeBlob()
-* (bug 6743) Don't link broken image links to the upload form when uploads
-  are disabled
-* (bug 9679) Improve documentation for $wgSiteNotice
-* (bug 10215) Show custom editing introduction when editing existing pages
-* (bug 10223) Fix edit link in noarticletext localizations for fr, oc
-* (bug 10247) Fix IP address regex to avoid false positive IPv6 matches
-* (bug 9948) Workaround for diff regression with old Mozilla versions
-* (bug 10265) Fix regression in category image gallery paging
-* (bug 8577) Fix some weird misapplications of time zones.
-  {{CURRENT*}} functions now consistently use UTC as intended, while
-  {{LOCAL*}} functions return local time per server config or $wgLocaltimezone.
-  Signature dates for Japanese and other languages including weekday now show
-  the correct day to match the rest of the time in local time.
-* Escape the output of magic variables that return page name or part of it
-* (bug 10309) Initialise parser state properly in extractSections(), fixes
-  some cases where section edits broke because tags were improperly stripped
-* Avoid PHP notice errors when doing HTTP proxy purges for an empty list
-* As intended, *skip* the HTTP proxy purges when doing HTCP purges
-* (bug 9696) Fix handling of brace transformations in "pagemovedtext"
-* (bug 10325) Fix regression in form action on Special:Listusers
-* Fixed installation on MyISAM or old InnoDB with charset=utf8, was giving 
-  overlong key errors.
-* Fixed zero-padding issues with MySQL 5 binary schema
-* (bug 10344) Don't follow a redirect after changing its protection level
-* (bug 10333) Correct date format in Slovenian
-* (bug 10160) Show error message for unknown namespace on Special:Allpages and
-  Special:Prefixindex; making forms prettier for RTL wikis.
-* (bug 10334) Replace normal spaces before percent (%) signs with non-breaking
-  spaces
-* (bug 10372) namespaceDupes.php no longer ignores namespace aliases
-* (bug 10198) namespaceDupes.php no longer ignores interwiki prefixes
-* namespaceDupes.php should work better for initial-lowercase wikis
-* (bug 10377) "Permanent links" to revisions still work if the page is moved
-  and the redirect deleted
-* (bug 7071) Properly handle an 'oldid' passed to view or edit that doesn't
-  match the given title. Fixes inconsistencies with talk, history, edit links.
-* (bug 10397) Fix AJAX watch error fallback when we receive a bogus result
-* (bug 10396) Fix AJAX error when $wgScriptPath/index.php is not valid;
-  using $wgScript now included in JS info
-* Use native XMLHttpRequest class in preference to ActiveX on IE 7; this
-  avoids the "ActiveX "Do you want to allow ActiveX?" prompt when something
-  security settings are cranked this way and AJAX-y gets used.
-* Delay AJAX watch initialization until click so IE 6 with ugly security
-  settings doesn't prompt you until you use the link.
-* (bug 10401) Provide non-redirecting link to original title in Special:Movepage
-* Fix broken handling of log views for page titles consisting of one
-  or more zeros, e.g. "0", "00" etc.
-* Fix read permission check for special pages with subpage parameters, e.g.
-  Special:Confirmemail
-* Fix read permission check for unreadable page titles which are numerically
-  equivalent to a whitelisted title
-* '?>' closing tag removed from all files to help avoid problems with extraneous
-  whitespace (broken XML feeds, etc.)
-* Don't use garbled parser cache output when viewing custom CSS or JavaScript
-  pages
-* (bug 10406) Fix Special:Listusers filter form for non-ASCII localizations
-* Fix empty message checks for message names containing &
-  This corrects some odd behavior with sidebar items and custom namespaces
-  containing ampersands.
-* (bug 10375) Change thousands separator character to   for Latin (la)
-* (bug 10477) Fix AJAX watch for Farsi on Firefox: JavaScript encoding tweak
-* (bug 10496) Fix broken DISTINCT option logic in database backend
-* Fix CSS media declaration for "screen, projection"; was causing some
-  validation issues
-* (bug 10495) $wgMemcachedDebug set twice in includes/DefaultSettings.php
-* (bug 10316) Prevent inconsistent cached skin settings in gen=js by setting
-  the intended skin directly in the URL.
-* (bug 9903) Don't mark redirects in categories as stubs
-* (bug 6965) Cannot include "Template:R" with {{R}} (magic word conflict)
-* Padding parser functions now work with strings like '0' that evaluate to false
-* (bug 10332) Title->userCan( 'edit' ) may return false positive
-* Fix bug with  in front of links for wikis where linkPrefixExtension is true
-* (bug 10552) Suppress rollback link in history for single-revision pages
-* (bug 10538) Gracefully handle invalid input on move success page
-* Fix for Esperanto double-x-encoding in move success page
-* (bug 10526) Fix toolbar/insertTags behavior for IE 6/7 and Opera (8+)
-  Now matches the selection behavior on Mozilla / Safari.
-  Patch by Alex Smotrov.
-* Don't show non-functional toolbar buttons on Opera 7 anymore
-* (bug 9151) Fix relative subpage links with section fragments
-* (bug 10560) Adding a space between category letter heading and "continues"
-* (bug 4650) Keep impossibly large/small counts off Special:Statistics
-* (bug 10608) PHP notice when installing with PostgreSQL
-* (bug 10615) Fix for transwiki import when CURL not available
-* (bug 8054) Return search page for empty search requests with ugly URLs
-* (bug 10572) Force refresh after clearing visitation timestamps on watchlist
-* (bug 10631) Warn when illegal characters are removed from filename at upload
-* Fix several JavaScript bugs under MSIE 5/Macintosh
-* (bug 10591) Use Arabic numerals (0,1,2...) for the Malayam language
-* (bug 10642) Fix shift-click checkbox behavior for Opera 9.0+ and 6.0
-* Work around Safari bug with pages ending in ".gz" or ".tgz"
-* Removed obsolete maintenance/changeuser.sql script; use RenameUser extension
-* (bug 2735) "Preview" shown in title bar for action=submit on special pages
-* Removed "restore" links from the deletion log embedded in Special:Undelete
-* Improved error reporting and robustness for file delete/undelete.
-* Improved speed of file delete by storing the SHA-1 hash in image/oldimage
-* Fixed leading zero in base 36 SHA-1 hash
-* Protection form no longer produces JavaScript errors
-* (bug 10741) File histories show "delete" links for non-sysops
-* (bug 10744) Treat "noarticletext" and "noarticletextanon" as wiki text when
-  used on a non-existent page with "action=info"
-* Fix escaping of raw message text when used on a non-existent page with
-  "action=info"
-* (bug 10683) Fix inconsistent handling of URL-encoded titles in links
-  used in redirects (i.e. they now work)
-* (bug 8878) Changes to $dateFormats in German localization (removing unused,
-  nonexistent formats, putting time after date)
-* (bug 10769) Database::update() should return boolean result
-* Fix preference checkbox display for right-to-left languages which caused
-  them to be hidden in IE in some cases
-* Fix upload form display in right-to-left languages
-* Fixed regression in blocking of username '0'
-* (bug 9437) Don't overwrite edit form submission handler when setting up
-  edit box scroll position preserve/restore behaviour
-* (bug 10805) Fix "undo" link when viewing the diff of the most recent
-  change to a page using "diff=0"
-* (bug 10765) img_auth.php will now refuse logged-out requests where
-  $wgWhitelistRead is undefined, instead of (incorrectly) honouring them
-* Fixed img_auth.php file name extraction for whitelist checking
-* Tweak spacing of email preference display
-* Table sorting JavaScript prefers textContent over innerText to allow hidden
-  sort keys to work on Safari
-* (bug 4530) Fix local name of Kurdish language
-* (bug 10830) Fix local name of Haitian Creole language
-* Fix invalid XHTML in Special:Protectedpages
-* Fix comments in contributions and log pages for right-to-left languages
-* Make installer include_path-independent, so it should work on hosts which
-  disable user setting of PHP include_path setting
-* glob() is horribly unreliable and doesn't work on some systems, including
-  free.fr shared hosting. No longer using it in Language::getLanguageNames()
-* (bug 10763) Fix multi-insert logic for PostgreSQL
-* Fix invalid XHTML when viewing a deleted revision
-* Fix syntax error in translations of magic words in Romanian language
-* (bug 8737) Fix warnings caused by incorrect use of `/dev/null` when piping
-  process error output under Windows
-* (bug 7890) Don't list redirects to special pages in Special:BrokenRedirects
-* (bug 10783) Resizing PNG-24 images with GD no longer causes all alpha
-  channel transparency to be lost and transparent pixels to be turned black
-* (bug 9339) General error pages were transforming messages and their parameters
-  in the wrong order
-* (bug 9026) Incorrect heading numbering when viewing Special:Statistics with
-  "auto-numbered headings" enabled
-* Fixed invalid XHTML in Special:Upload
-* (bug 11013) Make sure dl() is available before attempting to use it to check
-  available databases in installer
-* Resizing transparent GIF images with GD now retains transparency by skipping
-  resampling
-* (bug 11065) Fix regression in handling of wiki-formatted EXIF metadata
-* Double encoding broke Special:Newpages for some languages
-* Adding a newline before the statistics footer, to prevent parsing problems
-* Preventing the TOC from appearing in Special:Statistics
-* (bug 11082) Fix check for fully-specced table names in Database::tableName
-* (bug 11067) Fix regression in upload conflict thumbnail display
-* (bug 10985) Resolved cached entries on Special:DoubleRedirects were being
-  supressed, breaking paging - now strikes out "fixed" results
-* (bug 8393)  and  need to be preserved (without attributes) for
-  entries in the table of contents
-* (bug 11114) Fix regression in read-only mode error display during editing
-* Force non-MySQL databases to use an ORDER BY in SpecialAllpages to ensure 
-  that the first page_title is truly the first page title.
-* (bug 10836) Change the summary on creating of new section
-* Inclusion of Special:Wantedpages now works again
-
-== API changes since 1.10 ==
+=== Configuration changes in 1.12 ===
+* Marking edits as bot edits with Special:Contributions?bot=1 now requires the
+  markbotedit permission, rather than the rollback permission previously used.
+  This permission is assigned by default to the sysop group.
+* MediaWiki now checks if serialized files are out of date. New configuration
+  variable $wgCheckSerialized can be set to false to enable old behavior (i.e.
+  to not check and assume they are always up to date)
+* The rollback permission can now be rate-limited using the normal mechanism.
+* New configuration variable $wgExtraLanguageNames
+* Behaviour of $wgAddGroups and $wgRemoveGroups changed. New behaviour:
+* * Granting the userrights privilege allows arbitrary changing of rights.
+* * Without the userrights privilege, a user will be able to add and/or
+     remove the groups specified in $wgAddGroups and $wgRemoveGroups for
+     any groups they are in.
+* New permission userrights-interwiki for changing user rights on foreign wikis.
+* $wgImplictGroups for groups that are hidden from Special:Listusers, etc.
+* $wgAutopromote: automatically promote users who match specified criteria
+* $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf: allow users to add or remove
+  themselves from specified groups via Special:Userrights.
+* When $wgUseTidy has been enabled, PHP's Tidy module is now used if it is
+  present, in preference to an external Tidy executable which may or may not
+  be present. To force use of external Tidy even when the PHP module is
+  available, set $wgTidyInternal to false.
+
+
+=== New features in 1.12 ===
+* (bug 10735) Add a warning for non-descriptive filenames at Special:Upload
+* Add {{filepath:}} parser function to get full path to an uploaded file,
+  complementing {{fullurl:}} for pages.
+* (bug 11136) If using Postgres, search path is explicitly set if wgDBmwschema
+  is not set to 'mediawiki', allowing multiple mediawiki instances per user.
+* (bug 11151) Add descriptive  to revision history page
+* (bug 5412) Add feed links for the site to all pages
+* (bug 11353) Add ability to retrieve raw section content via action=raw
+* (bug 6909) Show relevant deletion log lines when uploading a previously
+  deleted file
+* On SkinTemplate based skins (like MonoBook), omit confusing "edit"/"view
+  source" tab entirely if the page doesn't exist and the user isn't allowed to
+  create it
+* Clarify instructions given when an exception is thrown
+* AuthPlugin added strictUserAuth() method to allow per-user override
+  of the strict() authentication behavior.
+* (bug 7872) Deleted revisions can now be viewed as diffs showing changes
+  against the previous revision, whether currently deleted or live.
+* Added tooltips for the "Go" and "Search" buttons
+* (bug 11649) Show input form when Special:Whatlinkshere has no parameters
+* isValidEmailAddr hook added to User method of that name, to allow, e.g., re-
+  stricting e-mail addresses to a specific domain
+* Removed "Clear" link in watchlist editor tools, as people were afraid to
+  click it. Existing clear links will fall back to the raw editor, which is
+  very easy to clear your watchlist with.
+* (bug 1405) Add wgUseNPPatrol option to control patroling for new articles
+  on Special:Newpages
+* LogLine hook added to allow formatting custom entries in Special:Log.
+* Support for Iranian calendar
+* (bug 1401) Allow hiding logged-in users, bots and patrolled pages on
+  Special:Newpages
+* ChangesListInsertArticleLink hook added for adding extra article info to RC.
+* MediaWikiPerformAction hook added for diverting control after the main
+  globals have been set up but before any actions have been taken.
+* BeforeWatchlist hook added for filtering or replacing watchlist.
+* SkinTemplateTabAction hook added for altering the properties of tab links.
+* OutputPage::getRedirect public method added.
+* (bug 11848, 12506) Allow URL parameters 'section', 'editintro' and 'preload'
+  in Special:Mypage and Special:Mytalk
+* Add ot=raw to Special:Allmessages
+* Support for Hebrew calendar
+* Support for Hebrew numerals in dates and times
+* (bug 11315) Signatures can be configured in [[MediaWiki:Signature]] and
+  [[MediaWiki:Signature-anon]]
+* Signatures for anonymous users link to Special:Contributions page rather than
+  user page
+* Added --override switch for disabled pages in updateSpecialPages.php
+* Provide a unique message (ipb_blocked_as_range) if unblock of a single IP
+  fails
+  because it is part of a blocked range.
+* (bug 3973) Use a separate message for the email content when an account is
+  created by another user
+* dumpTextPass.php can spawn fetchText.php as a subprocess, which should restart
+  cleanly if database connections fail unpleasantly.
+* (bug 12028) Add Special:Listbots as shortcut for Special:Listusers/bot
+* (bug 9633) Add a predefined list of delete reasons to the deletion form
+* Show a warning message when creating/editing a user (talk) page but the user
+  does not exists
+* (bug 8396) Ignore out-of-date serialised message caches
+* (bug 12195) Undeleting pages now requires 'undelete' permission
+* (bug 11810) Localize displayed semicolons
+* (bug 11657) Support for Thai solar calendar
+* (bug 943) RSS feed for Recentchangeslinked
+* Introduced AbortMove hook
+* (bug 2919) Protection of nonexistent pages with regular protection interface.
+* Special:Upload now lists permitted/prohibited file extensions.
+* Split ambiguous filetype-badtype message into two new messages,
+  filetype-unwanted-type and filetype-banned-type.
+* Added link to the old title in Special:Movepage
+* On Special:Movepage, errors are now more noticeable.
+* It is now possible to change rights on other local wikis without the MakeSysop
+  extension
+* Add HTML ID's mw-read-only-warning and mw-anon-edit-warning to warnings when
+  editing to allow CSS styling.
+* Parser now returns list of sections
+* When a user is prohibited from creating a page, a title of "View source"
+  makes no sense, and there should be no "Return to [[Page]]" link.
+* (bug 12486) Protected titles now give a warning for privileged editors.
+* (bug 9939) Special:Search now sets focus to search input box when no existing
+  search is active
+* For Special:Userrights, use GET instead of POST to search for users.
+* Allow subpage syntax for Special:Userrights, i.e., Special:Userrights/Name.
+* When submitting changes on Special:Userrights, show the full form again, not
+  just the search box.
+* Added exception hooks
+* (bug 12574) Allow bots to specify whether an edit should be marked as a bot
+  edit, via the parameter 'bot'. (Default: '1')
+* (bug 12536) User should be able to get MediaWiki version from any page
+* (bug 12622) A JavaScript constant to declare whether api.php is available
+* Add caching to the AJAX search
+* Add APCOND_INGROUPS
+* Add DBA caching to installer
+* (bug 18585) Added a bunch of parameters to the revertpage message
+* Support redirects in image namespace
+* (bug 10049) Prefix index search and namespaces in Special:Withoutinterwiki
+* (bug 12668) Support for custom iPhone bookmark icon via $wgAppleTouchIcon
+* Add option to include templates in Special:Export.
+* (bug 12655) Added $wgUserEmailUseReplyTo config option to put sender
+  address in Reply-To instead of From for user-to-user emails.
+  This protects against SPF problems and privacy-leaking bounce messages
+  when using mailers that set the envelope sender to the From header value.
+* (bug 11897) Add alias [[Special:CreateAccount]] & [[Special:Userlogin/signup]]
+  for Special:Userlogin?type=signup
+* (bug 12214) Add a predefined list of delete reasons to the file deletion form
+* Merged backends for OpenSearch suggestions and AJAX search.
+  Both now accept namespace prefixes, handle 'Media:' and 'Special:' pages,
+  and reject interwiki prefixes. PrefixSearch class centralizes this code,
+  and the backend part can be overridden by the PrefixSearchBackend hook.
+* (bug 10365) Localization of Special:Version
+* When installing using Postgres, the Pl/Pgsql language is now checked for 
+  and installed when at the superuser level.
+* The default robot policy for the entire wiki is now configurable via the
+  $wgDefaultRobotPolicy setting.
+* (bug 12239) Use different separators for autocomments
+* (bug 12857) Patrol link on new pages should clear floats
+* (bug 12968) Render redirect wikilinks in a redirect class for customization
+  via user/site CSS.
+* EditPageBeforeEditButtons hook added for altering the edit buttons below the edit box
+
+=== Bug fixes in 1.12 ===
+
+* Subpages are now indexed for searching properly when using PostgreSQL
+* (bug 3846) Suppress warnings from, e.g. open_basedir when scanning for
+  ImageMagick, diff3 et al. during installation [patch by Jan Reininghaus]
+* (bug 7027) Shift handling of deletion permissions-checking to
+  getUserPermissionsErrors.
+* Login and signup forms are now more correct for right-to-left languages.
+* (bug 5387) Block log items on RecentChanges don't make use of possible
+  translations
+* (bug 11211) Pass, as a parameter to the protectedpagetext interface
+  message, the level of protection.
+* (bug 9611) Supply the blocker and reason for the cantcreateaccounttext
+  message.
+* (bug 8759) Fixed bug where rollback was allowed on protected pages for wikis
+  where rollback is given to non-sysops.
+* (bug 8834) Split off permission for editing user JavaScript and CSS from
+  editinterface to a new permission key editusercssjs.
+* (bug 11266) Set fallback language for Fulfulde (ff) to French
+* (bug 11179) Include image version deletion comment in public log
+* Fixed notice when accessing special page without read permission and whitelist
+  is not defined
+* (bug 9252) Fix for tidy funkiness when using editintro mode
+* (bug 4021) Fix for MySQL wildcard search
+* (bug 10699) Fix for MySQL phrase search
+* (bug 11321) Fix width of gallerybox when option "width=xxx" is used
+* (bug 7890) Special:BrokenRedirects links deleted redirects to a non-existent
+  page
+* Fix initial statistics when installing: add correct values
+* (bug 11342) Fix several 'returnto' links in permissions/error pages which
+  linked to the main page instead of targetted page
+* Strike the link to the redirect rather than using an asterisk in
+  Special:Listredirects
+* (bug 11355) Fix false positives in Safe Mode and other config detection
+  when boolean settings are disabled with 'Off' via php_admin_value/php_value
+* (bug 11292) Fixed unserialize errors with Postgres by creating special Blob
+  object.
+* (bug 11363) Make all metadata fields bytea when using Postgres.
+* (bug 11331) Add buildConcat() and use CASE not IF for DB compatibility. Make
+  oldimage cascade delete via image table for Postgres, change fa_storage_key
+  TEXT.
+* (bug 11438) Live Preview chops returned text
+* Show the right message on account creation when the user is blocked
+* (bug 11450) Fix creation of objectcache table on upgrade
+* Fix namespace selection after submit of Special:Newpages
+* Make input form of Special:Newpages nicer for RTL wikis
+* (bug 11462) Fix typo in LanguageGetSpecialPageAliases hook name
+* (bug 11474) Fix unintentional fall-through in math error handling
+* (bug 11478) Fix undefined method call in file deletion interface
+* (bug 278) Search results no longer highlight incorrect partial word matches
+* Compatibility with incorrectly detected old-style DJVU mime types
+* (bug 11560) Fix broken HTML output from weird link nesting in edit comments.
+  Nested links (as in image caption text) still don't work _right_ but they're
+  less wrong
+* (bug 9718) Remove unnecessary css from main.css causing spacing issues on
+  some browsers.
+* (bug 11574) Add an interface message loginstart, which, similarly to loginend,
+  appears just before the login form. Patch by MinuteElectron.
+* Do not cache category pages if using 'from' or 'until'
+* Created new hook getUserPermissionsErrors, to go with userCan changes.
+* Diff pages did not properly display css/js pages.
+* (bug 11620) Add call to User::isValidEmailAddr during accout creation.
+* (bug 11629) If $wgEmailConfirmToEdit is true, require people to supply an
+  email address when registering.
+* (bug 11612) Days to show in recent changes cannot be larger than 7
+* (bug 11131) Change filearchive width/height columns to int for Postgres
+* Support plural in undeleted{revisions,revisions-files,files}
+* (bug 11343) If the database is read-only, ensure that undelete fails.
+* (bug 11690) Show revert link for page moves in Special:Log to allowed users
+  only
+* Initial-lowercase prefix checks in namespaceDupes.php now actually work.
+* Fix regression in LinkBatch.php breaking PHP 5.0
+* (bug 11452) wfMsgExt uses sometimes wrong language object for parsing magic
+  words when called with options ''parsemag'' or ''content''.
+* (bug 11727) Support plural in 'historysize' message
+* (bug 11744) Incorrect return value from Title::getParentCategories()
+* (bug 11762) Fix native language name of Akan (ak)
+* (bug 11722) Fix inconsistent case in unprotect tabs
+* (bug 11795) Be more paranoid about confirming accept-encoding header is
+  present
+* (bug 11809) Use formatNum() for more numbers
+* (bug 11818) Fix native language name of Inuktitut (iu)
+* Remove all commas when parsing float numbers in sorted tables
+* Limit text field of deletion, protection and user rights changes reasons to
+  255 characters (already restricted in the database)
+* In the deletion default reasons, calculate how much text to get from the
+  article text, rather than getting 150 characters (which may be too much)
+* Add two messages for Special:Blockme which were used but undefined
+* (bug 11921) Support plural in message number_of_watching_users_pageview
+* If an IP address is blocked as part of a rangeblock, attempting to unblock
+  the single IP should not unblock the entire range.
+* (bug 6695) Fix native language name of Southern Sotho (Sesotho) (st)
+* Make action=render follow redirects by default
+* If restricted read access was enabled, whitelist didn't work with special
+  pages which had spaces in theirs names
+* If restricted read access was enabled, requests for non-existing special pages
+  threw an exception
+* Feeds for recent changes now provide correct URLs for the change, not just
+  the page
+* Check for if IP is blocked as part of a range when unblocking (see above bug-
+  fix) was faulty. Now fixed.
+* Fixed wpReason URL parameter to action=delete.
+* Do not force a password for account creation by email
+* Ensure that rate-limiting is applied to rollbacks.
+* Make a better rate-limiting error message (i.e. a normal MW error,
+  rather than an "Internal Server Error").
+* Do not present an image bigger than the source when 'frameless' option is used
+  (to be consistent with the 'thumb' option now)
+* Support {{PLURAL}} for import log
+* Make sure that the correct log entries are shown on Special:Userrights even
+  for users with special characters in their names
+* The number of watching users in watchlists was always reported as 1
+* namespaceDupes.php no longer dies when coming across an illegal title
+* (bug 12143) Do not show a link to patrol new pages for non existent pages
+* (bug 12166) Fix XHTML validity for Special:Emailuser
+* (bug 11346) Users who cannot edit a page can now no longer unprotect it.
+* (bug 451) Add a generic Traditional / Simplified Chinese conversion table,
+  instead of a Traditional conversion with Taiwan variant, and a Simplified
+  conversion with China variant.
+* (bug 12178) Fix wpReason parameter to action=delete, again.
+* Graceful behavior for updateRestrictions.php if a page already has records
+  in the page_restrictions matching its old page_restrictions field.
+  May help with odd upgrade issues or race condition.
+* (bug 11993) Remove contentsub "revision history"
+* (bug 11952) Ensure we quote_ident() all schema names as needed
+   inside of the DatabasePostgres.php file.
+* (bug 12184) Exceptions now sent to stderr instead of stdout for command-line
+  scripts, making for cleaner reporting during batch jobs. PHP errors will also
+  be redirected in most cases on PHP 5.2.4 and later, switching 'display_errors'
+  to 'stderr' at runtime.
+* (bug 12148) Text highlight wasn't applied to cleanly deleted and added
+  lines in diff output
+* (bug 10166) Fix a PHP warning in Language::getMagic
+* Only mark rollback edits as minor if the user can normally mark edits minor
+* Escape page names in the move successful page (e.g. for pages with two
+  apostrophes).
+* (bug 12145) Add localized names of kk-variants
+* (bug 12259) Localize the numbers in deleted pages on the sysop view
+* Set proper page title for successful file deletion
+* (bug 11221) Do not show 'Compare selected versions' button for a history page
+  with one revision only
+* (bug 12267) Set the default date format to Thai solar calender for the Thai
+  language
+* (bug 10184) Extensions' stylesheets and scripts should be loaded before
+  user-customized ones (like Common.css, Common.js)
+* (bug 12283) Special:Newpages forgets parameters
+* (bug 12031) All namespaces doesn't work in Special:Newpages
+* (bug 585) Only create searchindex replica table for parser tests if db is
+  MySQL
+* Allow --record option if parserTests.php to work when using Postgres
+* (bug 12296) Simplify cache epoch in default LocalSettings.php
+* (bug 12346) XML fix when body double-click and click handlers are present
+* Fix regression -- missing feed links in sidebar on Special:Recentchanges
+* (bug 12371) Handle more namespace case variants in namespaceDupes.php
+* (bug 12380) Bot-friendly EditPage::spamPage
+* (bug 8066) Spaces can't be entered in special page aliases
+* Hide undo link if user can't edit article
+* (bug 12416) Fix password setting for createAndPromote.php
+* (bug 3097) Inconsistently usable titles containing HTML character entities
+  are now forbidden. A run of cleanupTitles.php will fix up existing pages.
+* (bug 12446) Permissions check fix for undelete link
+* (bug 12451) AJAX title normalization tweaks
+* When a user creating a page is not allowed to either create the page nor edit
+  it, all applicable reasons are now shown.
+* (bug 11428) Allow $wgScript inside $wgArticlePath when emulating PATH_INFO
+  Fixes 'root'-style rewrite configurations
+* (bug 12493) Removed hardcoded MAX_FILE_SIZE from Special:Import upload form
+* (bug 12489) Special:Userrights listed in restricted section again
+* (bug 12553) Fixed invalid XHTML in edit conflict screen
+* (bug 12505) Fixed section=0 with action=raw
+* (bug 12614) Do not log user rights change that didn't change anything
+* (bug 12584) Don't reset cl_timestamp when auto-updating sort key on move
+* (bug 12588) Fix selection in namespace selector on Special:Newpages
+* Use only default options when generating RSS and Atom syndication links.
+  This should help prevent infinite link loops that some software may follow,
+  and will generally keep feed behavior cleaner.
+* (bug 12608) Unifying the spelling of getDBkey() in the code.
+* (bug 12611) Bot flag ignored in recent changes
+* (bug 12617) Decimal and thousands separators for Romanian
+* (bug 12567) Fix for misformatted read-only messages on edit, protect.
+  Also added proper read-only checks to several special pages.
+  Have removed read-only checks from the general user permission framework.
+* Creating a site with a name containing '#' is no longer permitted, since the
+  name will not work (but $wgSiteName is not checked if manually set).
+* (bug 12695) Suppress dvips verbiage from web server error log
+* (bug 12716) Unprotecting a non-protected page leaves a log entry
+* Log username blocks with canonical form of name instead of input form
+* (bug 11593, 12719) Fixes for overzealous invocation of thumb.php.
+  Non-image handlers and full-size images may now decline it, fixing
+  mystery failures when using $wgThumbnailScriptPath.
+* (bug 12327) Comma in username no longer disrupts mail headers
+* (bug 6436) Localization of Special:Import XML parser Error message(s).
+* Security fix for API on MSIE
+* (bug 12768) Database query syntax error in maintenance/storage/compressOld.inc
+* (bug 12753) Empty captions in MediaWiki:Sidebar result in PHP errors
+* (bug 12790) Page protection is not logged when edit-protection is used
+  and move-protection is not
+* (bug 12793) Fix for restricted namespaces/pages in Special:Export
+* Fix for Special:Export so it doesn't ignore the page named '0'
+* Don't display rollback link if the user doesn't have all required permissions
+* The comment of a time-limited protection now contains the date in the default
+  format
+* (bug 12880) wfLoadExtensionMessages does not use $fallback from MessagesXx.php
+* (bug 12885) Correction for Russian convertPlural function
+* (bug 12768) Make DatabasePostgres->hasContraint() schema aware.
+* (bug 12735) Truncate usernames in comments using mb_ functions.
+* (bug 12892) Poor tab indexing on "delete file" form
+* (bug 12660) When creating an account by e-mail, do not send the creator's IP
+  address
+* (bug 12931) Fix wrong global variable in SpecialVersion
+* (bug 12919) Use 'deletedrevision' message as content when deleting an old file
+  version
+* (bug 12952) Using Nosuchusershort instead of Nosuchuser when account creation
+  is disabled
+* (bug 12869) Magnify icon alignment should be adjusted using linked CSS
+* Fixing message cache updates for MediaWiki messages moves
+* (bug 12815) Signature timestamps were always in UTC, even if the timezone code
+  in parentheses after them claimed otherwise
+* (bug 12732) Fix installer and searching to handle built-in tsearch2 for Postgres.
+* (bug 12784) Change "bool" types to smallint to handle Postgres 8.3 strictness.
+* (bug 12301) Allow maintenance/findhooks.php to search hooks in multiple directories.
+* (bug 7681, 11559) Cookie values no longer override GET and POST variables.
+* (bug 5262) Fully-qualified $wgStylePath no longer corrupted on XML feeds
+* (bug 3269) Inaccessible titles ending in '/.' or '/..' now forbidden.
+* (bug 12935, 12981) Fully-qualify archive URLs in delete, revert messages
+* (bug 12938) Fix template expansion and 404 returns for action=raw with section
+* (bug 11567) Fix error checking for PEAR::Mail. UserMailer::send() now returns
+  true-or-WikiError, which seems to be the calling convention expected by half
+  its callers already
+* (bug 12846) IE rtl.css issue in RTL wikis special:Preferences when selecting an
+  LTR user language
+* (bug 13005) DISPLAYTITLE does not work on preview
+* (bug 13004) Fix error on Postgres searches that return too many results.
+* (bug 13022) Fix upload from URL on PHP 5.0.x
+* (bug 13139, 13074) Fix request data for parameters with numeric names
+* (bug 13086) Trackbacks were returning invalid XML (extra whitespace)
+* (bug 12430) Fix call to private method LinkFilter::makeRegex fatal error in
+  maintenance/cleanupSpam.php
+* (bug 13211) Don't break edit buttons when Image namespace includes apostrophe
+* Fix regression with upgrades from 1.4 or below.
+* Fix regression: make dumpUploads.php work again
+* dumpUploads.php options now actually supported
+* wfRelativePath() no longer includes spurious ".." when base path is "/"
+* wfRelativePath() now returns full path for differing Windows drives
+* (bug 13274) Change link for message to ucfirst
+
+== Parser changes in 1.12 ==
+
+For help with migration to the MediaWiki 1.12 parser, please visit:
+
+http://meta.wikimedia.org/wiki/Migration_to_the_new_preprocessor
+
+The parser pass order has changed from
+
+   * Extension tag strip and render
+   * HTML normalisation and security
+   * Template expansion
+   * Main section...
+
+to
+
+   * Template and extension tag parse to intermediate representation
+   * Template expansion and extension rendering
+   * HTML normalisation and security
+   * Main section...
+
+The main effect of this for the user is that the rules for uncovered syntax
+have changed.
+
+Uncovered main-pass syntax, such as HTML tags, are now generally valid, whereas
+previously in some cases they were escaped. For example, you could have "<ta" in
+one template, and "ble>" in another template, and put them together to make a
+valid <table> tag. Previously the result would have been "<table>".
+
+Uncovered preprocessor syntax is generally not recognised. For example, if you
+have "{{a" in Template:A and "b}}" in Template:B, then "{{a}}{{b}}" will be
+converted to a literal "{{ab}}" rather than the contents of Template:Ab. This
+was the case previously in HTML output mode, and is now uniformly the case in
+the other modes as well. HTML-style comments uncovered by template expansion
+will not be recognised by the preprocessor and hence will not prevent template
+expansion within them, but they will be stripped by the following HTML security
+pass.
+
+Bug 5678 has been fixed. This has a number of user-visible effects related to
+the removal of this double-parse. Please see the wiki page for examples. 
+
+Message transformation mode has been removed, and replaced with "preprocess"
+mode. This means that some MediaWiki namespace messages may need to be updated,
+especially ones which took advantage of the terribly counterintuitive behaviour
+of the former message mode. 
+
+The header identification routines for section edit and for numbering section
+edit links have been merged. This removes a significant failure mode and fixes a
+whole category of bugs (tracked by bug #4899). Wikitext headings uncovered by
+template expansion will still be rendered into a heading tag, and will get an 
+entry in the TOC, but will not have a section edit link. HTML-style headings 
+will also not have a section edit link. Valid wikitext headings present in the 
+template source text will get a template section edit link. This is a major 
+break from previous behaviour, but I believe the effects are almost entirely 
+beneficial.
+
+The main motivation for making these changes was performance. The new two-pass
+preprocessor can skip "dead branches" in template expansion, such as unfollowed
+#switch cases and unused defaults for template arguments. This provides a
+significant performance improvement in template-heavy test cases taken from
+Wikipedia. Parser function hooks can participate in this performance improvement
+by using the new SFH_OBJECT_ARGS flag during registration.
+
+The pre-expand include size limit has been removed, since there's no efficient
+way to calculate such a figure, and it would now be meaningless for performance
+anyway. The "preprocessor node count" takes its place, with a generous default
+limit.
+
+The context in which XML-style extension tags are called has changed, so
+extensions which make use of the parser state may need compatibility changes.
+
+The new preprocessor syntax has been documented in Backus-Naur Form at:
+
+http://www.mediawiki.org/wiki/Preprocessor_ABNF
+
+The ExpandTemplates extension now has the ability to generate an XML parse 
+tree from wikitext source. This parse tree corresponds closely to the grammar
+documented on that page.
+
+=== API changes in 1.12 ===
 
 Full API documentation is available at http://www.mediawiki.org/wiki/API
 
-* New properties: links, templates, images, langlinks, categories, external
-  links
-* Breaking Change: imagelinks renamed into imageusage (il->iu)
-* Bug fix: incorrect generator behavior in some cases
-* JSON format allows an optional callback function to wrap the result.
-* Login module disabled until a more secure solution can be implemented
-* (bug 9938) Querying by revision identifier returns the most recent revision
-  for the corresponding page, rather than the requested revision
-* (bug 8772) Filter page revision queries by user
-* (bug 9927) User contributions queries do not accept IP addresses
-* Watchlist feed now reports a proper feed item when the user is not logged in
-* Watchlist feed date bug fixed - automatically shows one last day
-* Watchlist feed now allows to specify number of hours to monitor
-* list=allpages now returns a list instead of a map in JSON format
-* Breaking Change: in json, revisions are now returned as a list, not as a map.
-* Add: prop=info can show page is new flag, current page length, and visit
-  counter.
-* Change: Query watchlist now shows flags only when explicitly requested with
-  wlparam=flags
-* rc_this_oldid (textid) is no longer accessible from query watchlist
-* action=usercontribs: additional filtering by ucshow=; selection of needed
-  fields with ucprop=; the textid (rev_text_id) is no longer being exposed
-* (bug 9970) Breaking Change: backlinks, embeddedin and imageusage now return
-  lists in JSON instead of a map, and do not return anything when titles do
-  not exist
-* (bug 9121) Introduced indexpageids query parameter to list the page_id
-  values of all returned page items
-* (bug 10147) Now interwiki titles are not processed but added to a separate
-  "interwiki" section of the output.
-* Added categorymembers list to query for pages in a category.
-* (bug 10260) Show page protection status
-* (bug 10392) Include MediaWiki version details in version output
-* (bug 10411) Site language in meta=siteinfo
-* (bug 10391) action=help doesn't return help if format is fancy markup
-* backlinks, embeddedin and imageusage lists should use (bl|ei|iu)title parameter
-  instead of titles. Titles for these lists is obsolete and might stop working soon.
-* Added prop=imageinfo - gets image properties and upload history
-* (bug 10211) Added db server replication lag information in meta=siteinfo
-* Added external url search within wiki pages (list=exturlusage)
-* Added link enumeration (list=alllinks)
-* Added registered users enumeration (list=allusers)
-* Added full text search in titles and content (list=search)
-* (bug 10684) Expanded list=allusers functionality
-* Possible breaking change: prop=revisions no longer includes pageid for rvprop=ids
-* Added rvprop=size to prop=revisions (The size will not be shown if it is NULL in the database)
-* list=allpages now allows to filter by article min/max size and protection status
-* Added site statistics (siprop=statistics for meta=siteinfo)
-* (bug 10902) Unable to fetch user contributions from IP addresses
-* `list=usercontribs` no longer requires that the user exist
-* (bug 10971) `aufrom` parameter doesn't work with spaces
-* Fix username handling issue with `auprefix` parameter
-* Treat underscores as spaces for `aufrom` and `auprefix` parameters
-* Added edit/delete/... token retrieval to prop=info
-* Added meta=userinfo - logged-in user information, group membership, rights
-* (bug 11072) Fix regression in API image history query
-* (bug 11115) Adding SHA1 hash to imageinfo query
-* (bug 10898) API does not return an edit token for non-existent pages
-* (bug 10890) Timestamp support for categorymembers query
-* (bug 10980) Add exclude redirects on backlinks
-* IPv6 titles in User namespace are normalized (run cleanupTitles.php to fix any old stray pages)
-* Sysops now have the same limits on the number of items they can request in a query as bots.
-
-== Maintenance script changes since 1.10 ==
-
-* Add support for wgMaxTocLevel option in parserTests
-* (bug 6823) Disable article view counter in maintenance/dumpHTML.php
-* Fix maintenance/importImages.php so it doesn't barf PHP errors when no
-  suitable files are found, and make the list of extensions an option (defaults
-  to $wgFileExtensions)
-* Add option to maintenance/createAndPromote.php to give the user bureaucrat
-  permissions (--bureaucrat)
-* Allow overwriting existing files with a conflicting name using
-  maintenance/importImages.php
-* (bug 10266) Use native newlines when rebuilding a messages file.
-
-== Languages updated since 1.10 ==
+* (bug 11275) Enable descending sort in categorymembers
+* (bug 11308) Allow the API to output the image metadata
+* (bug 11296) Temporary fix for escaping of ampersands inside links in
+  pretty-printed
+  help document.
+* (bug 11405) Expand templates implementation in the API
+* (bug 11218) Add option to feedwatchlist to display multiple revisions for each
+  page.
+* (bug 11404) Provide name of exception caught in error code field of internal
+  api error messages.
+* (bug 11534) rvendid doesn't work
+* Fixed rvlimit of the revisions query to only enforce the lower query limit if
+  revision content is requested.
+* Include svn revision number (if install is checked-out from svn) in siteinfo
+  query.
+* (bug 11173) Allow limited wikicode rendering via api.php
+* (bug 11572) API should provide interface for expanding templates
+* (bug 11569) Login should return the cookie prefix
+* (bug 11632) Breaking change: Specify the type of a change in the recentchanges
+  list as 'edit', 'new', 'log' instead of 0, 1, 2, respectively.
+* Compatibility fix for PHP 5.0.x.
+* Add rctype parameter to list=recentchanges that filters by type
+* Add apprtype and apprlevel parameters to filter list=allpages by protection
+  types and levels
+* Add apdir parameter to enable listing all pages from Z to A
+* (bug 11721) Use a different title for results than for the help page.
+* (bug 11562) Added a user_registration parameter/field to the list=allusers
+  query.
+* (bug 11588) Preserve document structure for empty dataset in backlinks query.
+* Outputting list of all user preferences rather than having to request them by
+  name
+* (bug 11206) api.php should honor maxlag
+* Make prop=info check for restrictions in the old format too.
+* Add apihighlimits permission, default for sysops and bots
+* Add limit=max to use maximal limit
+* Add action=parse to render parser output. Use it instead of action=render
+  which has been removed
+* Add rvtoken=rollback to prop=revisions
+* Add meta=allmessages to get messages from site's messages cache.
+* Use bold and italics highlighting only in API help
+* Added action={block,delete,move,protect,rollback,unblock,undelete} and
+  list={blocks,deletedrevs}
+* Fixed sessionid attribute in action=login
+* Standardized limits. Revisions and Deletedrevisions formerly using
+  200 / 10000, now 500 / 5000, in line with other modules.
+* Added list=allcategories module
+* (bug 12321) API list=blocks reveals private data
+* Fix output of wfSajaxSearch
+* (bug 12413) meta=userinfo missing <query> tag
+* Add list of sections to action=parse output
+* Added action=logout
+* Added cascade flag to prop=info&inprop=protections
+* Added wlshow parameter to list=watchlist, similar to rcshow
+  (list=recentchanges)
+* Added support for image thumbnailing to prop=imageinfo
+* action={login,block,delete,move,protect,rollback,unblock,undelete} now must be
+  POSTed
+* prop=imageinfo interface changed: iihistory replaced by iilimit, iistart and
+  iiend parameters
+* Added amlang parameter to meta=allmessages
+* Added apfilterlanglinks parameter to list=allpages, replacing
+  query.php?what=nolanglinks
+* (bug 12718) Added action=paraminfo module that provides information about API
+  modules and their parameters
+* Added iiurlwidth and iiurlheight parameters to prop=imageinfo
+* Added format=txt and format=dbg, imported from query.php
+* Added uiprop=editcount to meta=userinfo
+* Added list=users which fetches user information
+* Added list=random which fetches a list of random pages
+* Added page parameter to action=parse to facilitate parsing of existing pages
+* Added uiprop=ratelimits to meta=userinfo
+* Added siprop=namespacealiases to meta=siteinfo
+* Made multiple values for ucuser possible in list=usercontribs
+* (bug 12944) Added cmstart and cmend parameters to list=categorymembers
+* Allow queries to have a where range that does not match the range field
+
+=== Languages updated in 1.12 ===
 
 * Afrikaans (af)
+* Akan (ak) (new)
+* Amharic (am) (new)
+* Aragonese (an)
+* Old English (ang) (new)
 * Arabic (ar)
-* Bikol (bcl)
+* Aramaic (arc)
+* Mapudungun (arn) (new)
+* Assamese (as)
+* Asturian (ast)
+* Avaric (av)
+* Kotava (avk) (new)
+* Aymara (ay)
+* Samogitian (bat-smg)
+* Boarisch (bar)
+* Bikol Central (bcl)
+* Belarusian (be)
+* Belarusian Taraskievica orthography (be-tarask)
 * Bulgarian (bg)
+* Bislama (bi) (new)
+* Bamanankan (bm)
+* Bengali (bn)
+* Bishnupriya Manipuri (bpy)
+* Breton (br)
+* Buginese (bug) (new)
 * Catalan (ca)
+* Zamboangueño (cbk-zam) (new)
+* Min Dong (cdo) (new)
+* Chechen (ce)
+* Cebuano (ceb) (new)
+* Cherokee (chr) (new)
+* Corsican (co) (new)
+* Crimean Tatar (Cyrillic) (crh-cyrl) (new)
+* Crimean Tatar (Latin) (crh-latn) (new)
+* Czech (cs)
+* Cassubian (csb)
+* Old Church Slavonic (cu)
+* Welsh (cy)
 * Danish (da)
 * German (de)
+* German (de-formal) (new)
+* Zazaki (diq) (new)
+* Lower Sorbian (dsb) (new)
+* Middle Dutch (dum) (new)
+* Divehi (dv)
+* Ewe (ee) (new)
 * Greek (el)
-* Esperanto (eo)
+* Emiliano-Romagnolo (eml)
+* English (en)
 * Spanish (es)
 * Estonian (et)
+* Euskara (eu)
 * Extremaduran (ext)
-* Farsi (fa)
 * Finnish (fi)
-* Vöro (fiu-vro)
+* Persian (fa)
+* Fulah (ff)
+* Võro (fiu-vro)
+* Fijian (fj) (new)
+* Faroese (fo)
 * French (fr)
-* Français Cadien (frc) (new)
-* Franco-Provençal/Arpetan (frp)
+* Cajun French (frc)
+* Franco-Provençal (frp)
+* Frisian (fy)
+* Irish (ga)
+* Gagauz (gag) (new)
+* Gön-gnŷ (gan) (new)
+* Scottish Gaelic (gd) (new)
 * Galician (gl)
+* Gilaki (glk) (new)
+* Gothic (got) (new)
+* Ancient Greek (grc) (new)
+* Swiss German (gsw)
 * Hakka (hak)
+* Hawaiian (haw) (new)
 * Hebrew (he)
+* Croatian (hr)
 * Upper Sorbian (hsb)
-* Haitian (ht)
+* Haitian Creole French (ht)
+* Hungarian (hu)
+* Armenian (hy)
+* Interlingua (ia)
 * Indonesian (id)
+* Interlingue (ie) (new)
+* Igbo (ig) (new)
+* Eastern Canadian (Unified Canadian Aboriginal Syllabics) (ike-cans) (new)
+* Eastern Canadian (Latin) (ike-latn) (new)
+* Ingush (inh) (new)
+* Ido (io) (new)
 * Icelandic (is)
 * Italian (it)
 * Japanese (ja)
+* Jutish (jut) (new)
 * Georgian (ka)
+* Kara-Kalpak (kaa)
 * Kabyle (kab)
 * Kazakh (kk)
+* Kazakh Arabic (kk-arab) (new)
+* Kazakh (China) (kk-cn)
+* Kazakh Cyrillic (kk-cyrl) (new)
+* Kazakh (Kazakhstan) (kk-kz)
+* Kazakh Latin (kk-latn) (new)
+* Kazakh (Turkey) (kk-tr)
+* Kalaallisut (kl) (new)
+* Kannada (kn)
 * Korean (ko)
-* Kinaray-a (krj) (new)
-* Kurdish (ku)
+* Kölsch (ksh)
+* Kurdish (Arabic) (ku-arab)
+* Kurdish (Latin) (ku-latn)
+* Cornish (kw) (new)
+* Kirghiz (ky) (new)
 * Latin (la)
+* Ladino (lad) (new)
+* Luxembourgish (lb) (new)
+* Lingua Franca Nova (lfn) (new)
+* Lak (lbe) (new)
+* Ganda (lg)
+* Limbugian (li)
+* Líguru (lij) (new)
+* Lozi (loz) (new)
+* Lingala (ln)
 * Lao (lo)
 * Lithuanian (lt)
-* Latviešu (lv)
+* Maithili (mai) (new)
+* Moksha (mdf) (new)
+* Malagasy (mg) (new)
 * Malayalam (ml)
-* Bahasa Melayu (ms)
-* Burmese (my)
-* Low German (nds)
+* Macedonian (mk)
+* Marathi (mr)
+* Malay (ms)
+* Erzya (myv) (new)
+* Nauru (na) (new)
+* Nahuatl (nah)
+* Min-nan (nan)
+* Napolitan (nap)
+* Low Saxon (nds)
+* Dutch Low Saxon (nds-nl)
+* Nepali (ne)
+* Newari (new) (new)
 * Dutch (nl)
-* Norwegian (no)
+* Norwegian (nynorsk) (nn)
+* Norwegian (bokmål)‬ (no)
+* Novial (nov) (new)
+* Northern Sotho (nso) (new)
 * Occitan (oc)
-* Punjabi (Gurmukhi) (pa)
+* Pangasinan (pag) (new)
+* Pampanga (pam) (new)
+* Papiamento (pap) (new)
+* Deitsch (pdc) (new)
+* Pfälzisch (pfl) (new)
 * Polish (pl)
-* Piedmontese (pms)
-* Portuguese (pt)
-* Romani (rmy)
+* Piemontèis (pms)
+* Pontic (pnt) (new)
+* Pashto (ps)
+* Portugese (pt)
+* Quechua (qu)
+* Rhaeto-Romance (rm) (new)
 * Romanian (ro)
-* Aromanian (roa-rup)
 * Russian (ru)
+* Megleno-Romanian (ruq) (new)
+* Megleno-Romanian (Cyrillic script) (ruq-cyrl) (new)
+* Megleno-Romanian (Greek script) (ruq-grek) (new)
+* Megleno-Romanian (Latin script) (ruq-latn) (new)
 * Sakha (sah)
-* Sango (se) (new)
+* Sardinian (sc)
+* Sicilian (scn)
+* Scots (sco) (new)
+* Sindhi (sd)
+* Sassarese (sdc) (new)
+* Seri (sei) (new)
+* Sango (sg) (new)
+* Tachelhit (shi)
+* Sinhalese (si) (new)
 * Slovak (sk)
-* Slovenian (sl)
-* Shona (sn)
-* Somali (so)
-* Albanian (sq)
+* Samoan (sm) (new)
+* Southern Sami (sma) (new)
+* Serbian (Cyrillic) (sr-ec)
+* Swati (ss) (new)
+* Southern Sotho (st) (new)
+* Saterland Frisian (stq) (new)
 * Sundanese (su)
 * Swedish (sv)
+* Swahili (sw) (new)
 * Tamil (ta)
+* Teluga (te)
+* Tetun (tet) (new)
+* Tajik (tg)
 * Thai (th)
-* Tigrinya (ti)
-* Setswana (tn)
-* Tok Pisin (tpi)
+* Tagalog (tl) (new)
+* Tonga (to) (new)
+* Turkish (tr)
+* Tuvinian (tyv)
 * Uyghur (ug)
+* Uzbek (uz)
+* Venitian (vec)
+* Vietnamese (vi)
+* West Flemish (vls)
 * Volapük (vo)
-* Winaray (war) (new)
+* Walloon (wa)
+* Wolof (wo)
+* Wu (wuu) (new)
+* Xhosa (xh) (new)
+* Mingrelian (xmf) (new)
 * Yiddish (yi)
-* Old Chinese / Late Middle Chinese (zh-classical)
-* Chinese (PRC) (zh-cn)
+* Yoruba (yo) (new)
+* Cantonese (yue)
+* Zhuang (za)
+* Zealandic (zea)
+* Chinese (zh)
+* Old Chinese/Late Time Chinese (zh-classical)
+* Chinese (Simplified) (zh-hans)
+* Chinese (Traditional) (zh-hant)
 * Chinese (Taiwan) (zh-tw)
-* Cantonese (zh-yue)
+* Zulu (zu) (new)
 
 == Compatibility ==
 
-MediaWiki 1.11 requires PHP 5 (5.1 recommended). PHP 4 is no longer supported.
+MediaWiki 1.12 requires PHP 5 (5.1 recommended). PHP 4 is no longer supported.
 
 PHP 5.0.x fails on 64-bit systems due to serious bugs with array processing:
 http://bugs.php.net/bug.php?id=34879
@@ -695,14 +822,14 @@ At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases.
 
 == Upgrading ==
 
-1.11 has several database changes since 1.10, and will not work without schema
+1.12 has several database changes since 1.11, and will not work without schema
 updates.
 
 If upgrading from before 1.7, you may want to run refreshLinks.php to ensure
 new database fields are filled with data.
 
-If upgrading from before 1.11, and you are using a wiki as a commons repository, 
-make sure that it is updated as well. Otherwise, errors may arise due to 
+If upgrading from before 1.11, and you are using a wiki as a commons repository,
+make sure that it is updated as well. Otherwise, errors may arise due to
 database schema changes.
 
 If you are upgrading from MediaWiki 1.4.x or earlier, some major database
@@ -711,6 +838,7 @@ break. Don't forget to always back up your database before upgrading!
 
 See the file UPGRADE for more detailed upgrade instructions.
 
+
 === Caveats ===
 
 Some output, particularly involving user-supplied inline HTML, may not
@@ -719,8 +847,7 @@ set $wgMimeType = "application/xhtml+xml"; to test for remaining problem
 cases, but this is not recommended on live sites. (This must be set for
 MathML to display properly in Mozilla.)
 
-
-For notes on 1.10.x and older releases, see HISTORY.
+For notes on 1.11.x and older releases, see HISTORY.
 
 
 === Online documentation ===
diff --git a/UPGRADE b/UPGRADE
index fb600ff2..5be0014d 100644
--- a/UPGRADE
+++ b/UPGRADE
@@ -11,7 +11,7 @@ for information and workarounds to common issues.
 == Overview ==
 
 Comprehensive documentation on upgrading to the latest version of the software
-is available at http://www.mediawiki.org/wiki/Manual:Upgrading_MediaWiki.
+is available at http://www.mediawiki.org/wiki/Manual:Upgrading.
 
 === Consult the release notes ===
 
diff --git a/api.php5 b/api.php5
index 64088872..504098d3 100644
--- a/api.php5
+++ b/api.php5
@@ -1 +1 @@
-<?php require 'api.php'; ?>
+<?php require 'api.php'; 
\ No newline at end of file
diff --git a/config/index.php b/config/index.php
index 0d08123e..556819df 100644
--- a/config/index.php
+++ b/config/index.php
@@ -325,7 +325,7 @@ foreach (array_keys($ourdb) AS $db) {
 }
 print "</li>\n";
 
-if( ini_get( "register_globals" ) ) {
+if( wfIniGetBool( "register_globals" ) ) {
 	?>
 	<li>
 		<div style="font-size:110%">
@@ -339,7 +339,7 @@ if( ini_get( "register_globals" ) ) {
 
 $fatal = false;
 
-if( ini_get( "magic_quotes_runtime" ) ) {
+if( wfIniGetBool( "magic_quotes_runtime" ) ) {
 	$fatal = true;
 	?><li class='error'><strong>Fatal: <a href='http://www.php.net/manual/en/ref.info.php#ini.magic-quotes-runtime'>magic_quotes_runtime</a> is active!</strong>
 	This option corrupts data input unpredictably; you cannot install or use
@@ -347,7 +347,7 @@ if( ini_get( "magic_quotes_runtime" ) ) {
 	<?php
 }
 
-if( ini_get( "magic_quotes_sybase" ) ) {
+if( wfIniGetBool( "magic_quotes_sybase" ) ) {
 	$fatal = true;
 	?><li class='error'><strong>Fatal: <a href='http://www.php.net/manual/en/ref.sybase.php#ini.magic-quotes-sybase'>magic_quotes_sybase</a> is active!</strong>
 	This option corrupts data input unpredictably; you cannot install or use
@@ -355,7 +355,7 @@ if( ini_get( "magic_quotes_sybase" ) ) {
 	<?php
 }
 
-if( ini_get( "mbstring.func_overload" ) ) {
+if( wfIniGetBool( "mbstring.func_overload" ) ) {
 	$fatal = true;
 	?><li class='error'><strong>Fatal: <a href='http://www.php.net/manual/en/ref.mbstring.php#mbstring.overload'>mbstring.func_overload</a> is active!</strong>
 	This option causes errors and may corrupt data unpredictably;
@@ -363,7 +363,7 @@ if( ini_get( "mbstring.func_overload" ) ) {
 	<?php
 }
 
-if( ini_get( "zend.ze1_compatibility_mode" ) ) {
+if( wfIniGetBool( "zend.ze1_compatibility_mode" ) ) {
 	$fatal = true;
 	?><li class="error"><strong>Fatal: <a href="http://www.php.net/manual/en/ini.core.php">zend.ze1_compatibility_mode</a> is active!</strong>
 	This option causes horrible bugs with MediaWiki; you cannot install or use
@@ -376,7 +376,7 @@ if( $fatal ) {
 	dieout( "</ul><p>Cannot install MediaWiki.</p>" );
 }
 
-if( ini_get( "safe_mode" ) ) {
+if( wfIniGetBool( "safe_mode" ) ) {
 	$conf->safeMode = true;
 	?>
 	<li><b class='error'>Warning:</b> <strong>PHP's
@@ -478,6 +478,8 @@ if ( $conf->eaccel ) {
 	print "<li><a href=\"http://eaccelerator.sourceforge.net/\">eAccelerator</a> installed</li>\n";
 }
 
+$conf->dba = function_exists( 'dba_open' );
+
 if( !( $conf->turck || $conf->eaccel || $conf->apc || $conf->xcache ) ) {
 	echo( '<li>Couldn\'t find <a href="http://turck-mmcache.sourceforge.net">Turck MMCache</a>,
 		<a href="http://eaccelerator.sourceforge.net">eAccelerator</a>,
@@ -514,7 +516,7 @@ $conf->ImageMagick = false;
 $imcheck = array( "/usr/bin", "/opt/csw/bin", "/usr/local/bin", "/sw/bin", "/opt/local/bin" );
 foreach( $imcheck as $dir ) {
 	$im = "$dir/convert";
-	if( file_exists( $im ) ) {
+	if( @file_exists( $im ) ) {
 		print "<li>Found ImageMagick: <tt>$im</tt>; image thumbnailing will be enabled if you enable uploads.</li>\n";
 		$conf->ImageMagick = $im;
 		break;
@@ -599,8 +601,8 @@ print "<li style='font-weight:bold;color:green;font-size:110%'>Environment check
 /* Check for validity */
 $errs = array();
 
-if( $conf->Sitename == "" || $conf->Sitename == "MediaWiki" || $conf->Sitename == "Mediawiki" ) {
-	$errs["Sitename"] = "Must not be blank or \"MediaWiki\"";
+if( preg_match( '/^$|^mediawiki$|#/i', $conf->Sitename ) ) {
+	$errs["Sitename"] = "Must not be blank or \"MediaWiki\" and may not contain \"#\"";
 }
 if( $conf->DBuser == "" ) {
 	$errs["DBuser"] = "Must not be blank";
@@ -751,6 +753,8 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) {
 		$wgDBts2schema = $conf->DBts2schema;
 
 		$wgCommandLineMode = true;
+		if (! defined ( 'STDERR' ) )
+			define( 'STDERR', fopen("php://stderr", "wb"));
 		$wgUseDatabaseMessages = false; /* FIXME: For database failure */
 		require_once( "$IP/includes/Setup.php" );
 		chdir( "config" );
@@ -839,6 +843,7 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) {
 					$errs["RootPW"] = "and password";
 					continue;
 				}
+				$wgDatabase->initial_setup($conf->RootPW, 'postgres');
 			}
 			echo( "<li>Attempting to connect to database \"$wgDBname\" as \"$wgDBuser\"..." );
 			$wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1);
@@ -847,6 +852,7 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) {
 			} else {
 				$myver = $wgDatabase->getServerVersion();
 			}
+			$wgDatabase->initial_setup('', $wgDBname);
 		}
 
 		if ( !$wgDatabase->isOpen() ) {
@@ -1010,12 +1016,16 @@ if( $conf->posted && ( 0 == count( $errs ) ) ) {
 
 			print " done.</li>\n";
 
-			print "<li>Initializing data...</li>\n";
+			print "<li>Initializing statistics...</li>\n";
 			$wgDatabase->insert( 'site_stats',
 				array ( 'ss_row_id'        => 1,
 						'ss_total_views'   => 0,
-						'ss_total_edits'   => 0,
-						'ss_good_articles' => 0 ) );
+						'ss_total_edits'   => 1, # Main page first edit
+						'ss_good_articles' => 0, # Main page is not a good article - no internal link
+						'ss_total_pages'   => 1, # Main page
+						'ss_users'         => $conf->SysopName ? 1 : 0, # Sysop account, if created
+						'ss_admins'        => $conf->SysopName ? 1 : 0, # Sysop account, if created
+						'ss_images'        => 0 ) );
 
 			# Set up the "regular user" account *if we can, and if we need to*
 			if( $conf->Root and $conf->DBtype == 'mysql') {
@@ -1198,7 +1208,7 @@ if( count( $errs ) ) {
 	</p>
 
 	<div class="config-input">
-		<label class='column'>Shared memory caching:</label>
+		<label class='column'>Object caching:</label>
 
 		<ul class="plain">
 		<li><?php aField( $conf, "Shm", "No caching", "radio", "none" ); ?></li>
@@ -1223,6 +1233,11 @@ if( count( $errs ) ) {
 				aField( $conf, "Shm", "eAccelerator", "radio", "eaccel" );
 				echo "</li>";
 			}
+			if ( $conf->dba ) {
+				echo "<li>";
+				aField( $conf, "Shm", "DBA (not recommended)", "radio", "dba" );
+				echo "</li>";
+			}
 		?>
 		<li><?php aField( $conf, "Shm", "Memcached", "radio", "memcached" ); ?></li>
 		</ul>
@@ -1234,6 +1249,9 @@ if( count( $errs ) ) {
 		<br /><br />
 		MediaWiki can also detect and support eAccelerator, Turck MMCache, APC, and XCache, but
 		these should not be used if the wiki will be running on multiple application servers.
+		<br/><br/>
+		DBA (Berkeley-style DB) is generally slower than using no cache at all, and is only 
+		recommended for testing.
 	</p>
 </div>
 
@@ -1431,7 +1449,7 @@ window.onload = toggleDBarea('<?php echo $conf->DBtype; ?>',
 /* -------------------------------------------------------------------------------------- */
 function writeSuccessMessage() {
  $script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php';
-	if ( ini_get( 'safe_mode' ) && !ini_get( 'open_basedir' ) ) {
+	if ( wfIniGetBool( 'safe_mode' ) && !ini_get( 'open_basedir' ) ) {
 		echo <<<EOT
 <div class="success-box">
 <p>Installation successful!</p>
@@ -1464,6 +1482,9 @@ EOT;
 
 
 function escapePhpString( $string ) {
+	if ( is_array( $string ) || is_object( $string ) ) {
+		return false;
+	}
 	return strtr( $string,
 		array(
 			"\n" => "\\n",
@@ -1494,6 +1515,10 @@ function writeLocalSettings( $conf ) {
 			$cacheType = 'CACHE_ACCEL';
 			$mcservers = 'array()';
 			break;
+		case 'dba':
+			$cacheType = 'CACHE_DBA';
+			$mcservers = 'array()';
+			break;
 		default:
 			$cacheType = 'CACHE_NONE';
 			$mcservers = 'array()';
@@ -1666,9 +1691,8 @@ if ( \$wgCommandLineMode ) {
 
 # When you make changes to this configuration file, this will make
 # sure that cached pages are cleared.
-\$configdate = gmdate( 'YmdHis', @filemtime( __FILE__ ) );
-\$wgCacheEpoch = max( \$wgCacheEpoch, \$configdate );
-	"; ## End of setting the $localsettings string
+\$wgCacheEpoch = max( \$wgCacheEpoch, gmdate( 'YmdHis', @filemtime( __FILE__ ) ) );
+"; ## End of setting the $localsettings string
 
 	// Keep things in Unix line endings internally;
 	// the system will write out as local text type.
@@ -1790,7 +1814,7 @@ function locate_executable($loc, $names, $versioninfo = false) {
 
 	foreach ($names as $name) {
 		$command = "$loc".DIRECTORY_SEPARATOR."$name";
-		if (file_exists($command)) {
+		if (@file_exists($command)) {
 			if (!$versioninfo)
 				return $command;
 
@@ -1812,7 +1836,6 @@ function testMemcachedServer( $server ) {
 	}
 	if ( !$errstr && count( $hostport ) != 2 ) {
 		$errstr = 'Please specify host and port';
-		var_dump( $hostport );
 	}
 	if ( !$errstr ) {
 		list( $host, $port ) = $hostport;
@@ -1910,7 +1933,7 @@ function printListItem( $item ) {
 			<li><a href="http://meta.wikipedia.org/wiki/MediaWiki_User's_Guide">User's Guide</a></li>
 			<li><a href="http://meta.wikimedia.org/wiki/MediaWiki_FAQ">FAQ</a></li>
 		</ul>
-		<p style="font-size:90%;margin-top:1em">MediaWiki is Copyright © 2001-2007 by Magnus Manske, Brion Vibber, Lee Daniel Crocker, Tim Starling, Erik Möller, Gabriel Wicke and others.</p>
+		<p style="font-size:90%;margin-top:1em">MediaWiki is Copyright © 2001-2008 by Magnus Manske, Brion Vibber, Lee Daniel Crocker, Tim Starling, Erik Möller, Gabriel Wicke and others.</p>
 	</div></div>
 </div>
 
diff --git a/docs/globals.txt b/docs/globals.txt
index ecc5ab33..8320eec0 100644
--- a/docs/globals.txt
+++ b/docs/globals.txt
@@ -72,3 +72,13 @@ $wgParser
 
 $wgLoadBalancer
 	A LoadBalancer object, manages database connections.
+
+$wgRequest
+	WebRequest object, to get request data
+
+$wgMemc, $messageMemc, $parserMemc
+	Object caches
+
+$wgMessageCache
+	Message cache, to manage interface messages
+
diff --git a/docs/hooks.txt b/docs/hooks.txt
index 9614bead..9e27a8d0 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -245,12 +245,19 @@ $password: the password being submitted, not yet checked for validity
           default is LoginForm::ABORTED. Note that the client may be using
           a machine API rather than the HTML user interface.
 
+'AbortMove': allows to abort moving an article (title)
+$old: old title
+$nt: new title
+$user: user who is doing the move
+$err: error message
+
 'AbortNewAccount': Return false to cancel account creation.
 $user: the User object about to be created (read-only, incomplete)
 $message: out parameter: error message to display on abort
 
 'AddNewAccount': after a user account is created
 $user: the User object that was created. (Parameter added in 1.7)
+$byEmail: true when account was created "by email" (added in 1.12)
 
 'AjaxAddScript': Called in output page just before the initialisation
 of the javascript ajax engine. The hook is only called when ajax
@@ -261,6 +268,10 @@ before showing the edit form ( EditPage::edit() ). This is triggered
 on &action=edit.
 $EditPage : the EditPage object
 
+'ArticleAfterFetchContent': after fetching content of an article from the database
+$article: the article (object) being loaded from the database
+$content: the content (string) of the article
+
 'ArticleDelete': before an article is deleted
 $article: the article (object) being deleted
 $user: the user (object) deleting the article
@@ -271,6 +282,16 @@ $article: the article that was deleted
 $user: the user that deleted the article
 $reason: the reason the article was deleted
 
+'ArticleEditUpdateNewTalk': before updating user_newtalk when a user talk page was changed
+$article: article (object) of the user talk page
+
+'ArticleEditUpdatesDeleteFromRecentchanges': before deleting old entries from recentchanges table, return false to not delete old entries
+$article: article (object) being modified
+
+'ArticleFromTitle': when creating an article object from a title object using Wiki::articleFromTitle()
+$title: title (object) used to create the article object
+$article: article (object) that will be returned
+
 'ArticleInsertComplete': After an article is created
 $article: Article created
 $user: User creating the article
@@ -282,6 +303,18 @@ $section: (No longer used)
 $flags: Flags passed to Article::doEdit()
 $revision: New Revision of the article
 
+'ArticleMergeComplete': after merging to article using Special:Mergehistory
+$targetTitle: target title (object)
+$destTitle: destination title (object) 
+
+'ArticlePageDataAfter': after loading data of an article from the database
+$article: article (object) whose data were loaded
+$row: row (object) returned from the database server
+
+'ArticlePageDataBefore': before loading data of an article from the database
+$article: article (object) that data will be loaded
+$fields: fileds (array) to load from the database
+
 'ArticleProtect': before an article is protected
 $article: the article being protected
 $user: the user doing the protection
@@ -296,6 +329,19 @@ $protect: boolean whether it was a protect or an unprotect
 $reason: Reason for protect
 $moveonly: boolean whether it was for move only or not
 
+'ArticlePurge': before executing "&action=purge" 
+$article: article (object) to purge
+
+'ArticleRevisionUndeleted' after an article revision is restored
+$title: the article title
+$revision: the revision
+$oldPageID: the page ID of the revision when archived (may be null)
+
+'ArticleRollbackComplete': after an article rollback is completed
+$article: the article that was edited
+$user: the user who did the rollback
+$revision: the revision the page was reverted back to
+
 'ArticleSave': before an article is saved
 $article: the article (object) being saved
 $user: the user (object) saving the article
@@ -327,20 +373,24 @@ $section: section #
 
 wfRunHooks( 'ArticleSaveComplete', array( &$this, &$wgUser, $text, $summary, $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 
-'ArticleUndeleted': When one or more revisions of an article are restored
+'ArticleUndelete': When one or more revisions of an article are restored
 $title: Title corresponding to the article restored
 $create: Whether or not the restoration caused the page to be created
 (i.e. it didn't exist before)
 
-'ArticleViewHeader': Before the parser cache is about to be tried for article viewing.
-&$pcache: whether to try the parser cache or not
-&$outputDone: whether the output for this page finished or not
-
 'ArticleUpdateBeforeRedirect': After a page is updated (usually on save), before the user is redirected back to the page
 &$article: the article
 &$sectionanchor: The section anchor link (e.g. "#overview" )
 &$extraq: Extra query parameters which can be added via hooked functions
 
+'ArticleViewHeader': Before the parser cache is about to be tried for article viewing.
+&$article: the article
+&$pcache: whether to try the parser cache or not
+&$outputDone: whether the output for this page finished or not
+
+'ArticleViewRedirect': before setting "Redirected from ..." subtitle when follwed an redirect
+$article: target article (object)
+
 'AuthPluginSetup': update or replace authentication plugin object ($wgAuth)
 Gives a chance for an extension to set it programattically to a variable class.
 &$auth: the $wgAuth object, probably a stub
@@ -348,6 +398,12 @@ Gives a chance for an extension to set it programattically to a variable class.
 'AutoAuthenticate': called to authenticate users on external/environmental means
 $user: writes user object to this parameter
 
+'AutopromoteCondition': check autopromote condition for user.
+$type: condition type
+$args: arguments
+$user: user
+$result: result of checking autopromote condition
+
 'BadImage': When checking against the bad image list
 $name: Image name being checked
 &$bad: Whether or not the image is "bad"
@@ -379,6 +435,13 @@ $out: OutputPage object
 &$parser: Parser object
 &$ig: ImageGallery object
 
+'BeforeWatchlist': Override watchlist display or add extra SQL clauses.
+$nondefaults: Assoc array with the following keys:
+              days, hideOwn, hideBots, hideMinor, namespace
+$wgUser: wgUser.
+&$hookSql: a string which will be inserted without sanitation into the SQL query
+           used to get the watchlist, at the end of the WHERE part.
+
 'BlockIp': before an IP address or user is blocked
 $block: the Block object about to be saved
 $user: the user _doing_ the block (not the one being blocked)
@@ -394,6 +457,14 @@ $output: OutputPage object in use
 'CategoryPageView': before viewing a categorypage in CategoryPage::view
 $catpage: CategoryPage instance
 
+'ChangesListInsertArticleLink': Override or augment link to article in RC list.
+&$this:        ChangesList instance.
+&$articlelink: HTML of link to article (already filled-in).
+&$s:           HTML of row that is being constructed.
+&$rc:          RecentChange instance.
+$unpatrolled:  Whether or not we are showing unpatrolled changes.
+$watched:      Whether or not the change is watched by the user.
+
 'ContributionsToolLinks': Change tool links above Special:Contributions
 $id: User identifier
 $title: User page title
@@ -412,30 +483,46 @@ $diff: DifferenceEngine object that's calling
 $oldRev: Revision object of the "old" revision (may be null/invalid)
 $newRev: Revision object of the "new" revision
 
-'EditPage::attemptSave': called before an article is
-saved, that is before insertNewArticle() is called
-&$editpage_Obj: the current EditPage object
+'DisplayOldSubtitle': before creating subtitle when browsing old versions of an article
+$article: article (object) being viewed
+$oldid: oldid (int) being viewed
+
+'EditFilter': Perform checks on an edit
+$editor: Edit form (see includes/EditPage.php)
+$text: Contents of the edit box
+$section: Section being edited
+&$error: Error message to return
+
+'EditFilterMerged': Post-section-merge edit filter
+$editor: EditPage instance (object)
+$text: content of the edit box
+$error: error message to return
 
 'EditFormPreloadText': Allows population of the edit form when creating new pages
 &$text: Text to preload with
 &$title: Title object representing the page being created
 
+'EditPage::attemptSave': called before an article is
+saved, that is before insertNewArticle() is called
+&$editpage_Obj: the current EditPage object
+
 'EditPage::showEditForm:fields': allows injection of form field into edit form
 &$editor: the EditPage instance for reference
 &$out: an OutputPage instance to write to
 return value is ignored (should always return true)
 
-'EditFilter': Perform checks on an edit
-$editor: Edit form (see includes/EditPage.php)
-$text: Contents of the edit box
-$section: Section being edited
-&$error: Error message to return
+'EditPage::showEditForm:initial': before showing the edit form
+$editor: EditPage instance (object)
 
 Return false to halt editing; you'll need to handle error messages, etc. yourself.
 Alternatively, modifying $error and returning true will cause the contents of $error
 to be echoed at the top of the edit form as wikitext. Return true without altering
 $error to allow the edit to proceed.
 
+'EditPageBeforeEditButtons':  allows modifying the edit buttons below the textarea in the edit form
+&$editpage: The current EditPage object
+&$buttons: Array of edit buttons "Save", "Preview", "Live", and "Diff"
+
 'EditSectionLink': Override the return value of Linker::editSectionLink()
 $skin: Skin rendering the UI
 $title: Title being linked to
@@ -478,20 +565,42 @@ false override the list derivative used)
 'FileUpload': When a file upload occurs
 $file : Image object representing the file that was uploaded
 
+'GetBlockedStatus': after loading blocking status of an user from the database
+$user: user (object) being checked
+
+'GetFullURL': modify fully-qualified URLs used in redirects/export/offsite data
+$title: Title object of page
+$url: string value as output (out parameter, can modify)
+$query: query options passed to Title::getFullURL()
+
 'GetInternalURL': modify fully-qualified URLs used for squid cache purging
 $title: Title object of page
 $url: string value as output (out parameter, can modify)
 $query: query options passed to Title::getInternalURL()
 
+'GetLinkColours': modify the CSS class of an array of page links
+$linkcolour_ids: array of prefixed DB keys of the pages linked to, indexed by page_id.
+&$colours: (output) array of CSS classes, indexed by prefixed DB keys
+
 'GetLocalURL': modify local URLs as output into page links
 $title: Title object of page
 $url: string value as output (out parameter, can modify)
 $query: query options passed to Title::getLocalURL()
 
-'GetFullURL': modify fully-qualified URLs used in redirects/export/offsite data
-$title: Title object of page
-$url: string value as output (out parameter, can modify)
-$query: query options passed to Title::getFullURL()
+'getUserPermissionsErrors': Add a permissions error when permissions errors are
+	checked for.  Use instead of userCan for most cases. Return false if the
+	user can't do it, and populate $result with the reason in the form of
+	array( messagename, param1, param2, ... ).  For consistency, error messages
+	should be plain text with no special coloring, bolding, etc. to show that
+	they're errors; presenting them properly to the user as errors is done by
+	the caller.
+$title: Title object being checked against
+$user : Current user object
+$action: Action being checked
+$result: User permissions error to add. If none, return true.
+
+'getUserPermissionsErrorsExpensive': Absolutely the same, but is called only
+	if expensive checks are enabled.
 
 'ImageOpenShowImageInlineBefore': Call potential extension just before showing the image on an image page
 $imagePage: ImagePage object ($this)
@@ -507,20 +616,62 @@ after noinclude/includeonly/onlyinclude and other processing.
 &$text: string containing partially parsed text
 &$this->mStripState: Parser's internal StripState object
 
+'IsFileCacheable': Override the result of Article::isFileCacheable() (if true)
+$article: article (object) being checked
+
+'IsTrustedProxy': Override the result of wfIsTrustedProxy()
+$ip: IP being check
+$result: Change this value to override the result of wfIsTrustedProxy()
+
+'isValidEmailAddr': Override the result of User::isValidEmailAddr(), for ins-
+tance to return false if the domain name doesn't match your organization
+$addr: The e-mail address entered by the user
+&$result: Set this and return false to override the internal checks
+
 'isValidPassword': Override the result of User::isValidPassword()
-$password: Desired password
+$password: The password entered by the user
 &$result: Set this and return false to override the internal checks
 $user: User the password is being validated for
 
+'LanguageGetMagic': Use this to define synonyms of magic words depending of the language
+$magicExtensions: associative array of magic words synonyms
+$lang: laguage code (string)
+
+'LanguageGetSpecialPageAliases': Use to define aliases of special pages names depending of the language
+$specialPageAliases: associative array of magic words synonyms
+$lang: laguage code (string)
+
+'LinksUpdate': At the beginning of LinksUpdate::doUpdate() just before the actual update
+&$linksUpdate: the LinkUpdate object
+
+'LinksUpdateComplete': At the end of LinksUpdate::doUpdate() when updating has completed
+&$linksUpdate: the LinkUpdate object
+
 'LinksUpdateConstructed': At the end of LinksUpdate() is contruction.
 &$linksUpdate: the LinkUpdate object
 
+'LoadAllMessages': called by MessageCache::loadAllMessages() to load extensions messages
+
+'LoadExtensionSchemaUpdates': called by maintenance/updaters.inc when upgrading database schema
+
 'LoginAuthenticateAudit': a login attempt for a valid user account either succeeded or failed.
                           No return data is accepted; this hook is for auditing only.
 $user: the User object being authenticated against
 $password: the password being submitted and found wanting
 $retval: a LoginForm class constant with authenticateUserData() return value (SUCCESS, WRONG_PASS, etc)
 
+'LogLine': Processes a single log entry on Special:Log
+$log_type: string for the type of log entry (e.g. 'move'). Corresponds to logging.log_type 
+    database field.
+$log_action: string for the type of log action (e.g. 'delete', 'block', 'create2'). Corresponds
+    to logging.log_action database field.
+$title: Title object that corresponds to logging.log_namespace and logging.log_title database fields.
+$paramArray: Array of parameters that corresponds to logging.log_params field. Note that only $paramArray[0]
+    appears to contain anything.
+&$comment: string that corresponds to logging.log_comment database field, and which is displayed in the UI.
+&$revert: string that is displayed in the UI, similar to $comment.
+$time: timestamp of the log entry (added in 1.12) 
+
 'LogPageValidTypes': action being logged. DEPRECATED: Use $wgLogTypes
 &$type: array of strings
 
@@ -533,6 +684,12 @@ $retval: a LoginForm class constant with authenticateUserData() return value (SU
 'LogPageActionText': strings used by wfMsg as a header. DEPRECATED: Use $wgLogActions
 &$actionText: array of strings
 
+'MagicWordMagicWords': When defining new magic word. DEPRECATED: Use LanguageGetMagic hook instead
+$magicWords: array of strings
+
+'MagicWordwgVariableIDs': When definig new magic words IDs. DEPRECATED: Use LanguageGetMagic hook instead
+$variableIDs: array of strings
+
 'MarkPatrolled': before an edit is marked patrolled
 $rcid: ID of the revision to be marked patrolled
 $user: the user (object) marking the revision as patrolled
@@ -550,11 +707,31 @@ $mathRenderer: instance of MathRenderer
 $errmsg: error message, in HTML (string). Nonempty indicates failure
  of rendering the formula
 
+'MediaWikiPerformAction': Override MediaWiki::performAction().
+Use this to do something completely different, after the basic
+globals have been set up, but before ordinary actions take place.
+$output:  $wgOut
+$article: $wgArticle
+$title:   $wgTitle
+$user:    $wgUser
+$request: $wgRequest
+
+'MessagesPreLoad': When loading a message from the database
+$title: title of the message (string)
+$message: value (string), change it to the message you want to define
+
+'MonoBookTemplateToolboxEnd': Called by Monobook skin after toolbox links have been rendered (useful for adding more)
+$tools: array of tools
+
 'OutputPageBeforeHTML': a page has been processed by the parser and
 the resulting HTML is about to be displayed.  
 $parserOutput: the parserOutput (object) that corresponds to the page 
 $text: the text that will be displayed, in HTML (string)
 
+'OutputPageParserOutput': after adding a parserOutput to $wgOut
+$out: OutputPage instance (object)
+$parserOutput: parserOutput instance being added in $out
+
 'PageHistoryBeforeList': When a history page list is about to be constructed.
 $article: the article that the history is loading for
 
@@ -567,6 +744,53 @@ $s: the string representing this parsed line
   this hook and append its values to the key.
 $hash: reference to a hash key string which can be modified
 
+'ParserAfterStrip': Same as ParserBeforeStrip
+
+'ParserAfterTidy': Called after Parser::tidy() in Parser::parse()
+$parser: Parser object being used 
+$text: text that'll be returned
+
+'ParserBeforeInternalParse': called at the beginning of Parser::internalParse()
+$parser: Parser object
+$text: text to parse
+$stripState: StripState instance being used
+
+'ParserBeforeStrip': Called at start of parsing time (no more strip, deprecated ?)
+$parser: parser object
+$text: text being parsed
+$stripState: stripState used (object)
+
+'ParserBeforeTidy': called before tidy and custom tags replacements
+$parser: Parser object being used 
+$text: actual text
+
+'ParserClearState': called at the end of Parser::clearState()
+$parser: Parser object being cleared
+
+'ParserFirstCallInit': called when the ther parser initialises for the first time
+$parser: Parser object being cleared
+
+'ParserGetVariableValueSwitch': called when the parser need the value of a custom magic word
+$parser: Parser object
+$varCache: array to store the value in case of multiples calls of the same magic word
+$index: index (string) of the magic
+$ret: value of the magic word (the hook should set it)
+
+'ParserGetVariableValueTs': use this to change the value of the time for the {{LOCAL...}} magic word
+$parser: Parser object
+$time: actual time (timestamp)
+
+'ParserGetVariableValueVarCache': use this to change the value of the variable cache or return false to not use it
+$parser: Parser object
+$varCache: varaiable cache (array)
+
+'ParserLimitReport': called at the end of Parser:parse() when the parser will include comments about size of the text parsed
+$parser: Parser object
+$limitReport: text that will be included (without comment tags)
+
+'ParserTestParser': called when creating a new instance of Parser in maintenance/parserTests.inc
+$parser: Parser object created
+
 'ParserTestTables': alter the list of tables to duplicate when parser tests
 are run. Use when page save hooks require the presence of custom tables
 to ensure that tests continue to run properly.
@@ -589,10 +813,30 @@ the built-in rate limiting checks are used, if enabled.
 $form : PreferencesForm object
 &$html : HTML to append to
 
+'PrefixSearchBackend': Override the title prefix search used for OpenSearch and
+AJAX search suggestions. Put results into &$results outparam and return false.
+$ns : int namespace key to search in
+$search : search term (not guaranteed to be conveniently normalized)
+$limit : maximum number of results to return
+&$results : out param: array of page names (strings)
+
+'PrefsEmailAudit': called when user changes his email address
+$user: User (object) changing his email address
+$oldaddr: old email address (string)
+$newaddr: new email address (string)
+
+'PrefsPasswordAudit': called when user changes his password
+$user: User (object) changing his passoword
+$newPass: new password
+$error: error (string) 'badretype', 'wrongpassword', 'error' or 'success'
+
 'RawPageViewBeforeOutput': Right before the text is blown out in action=raw
 &$obj: RawPage object
 &$text: The text that's going to be the output
 
+'RecentChange_save': called at the end of RecenChange::save()
+$recentChange: RecentChange object
+
 'RenderPreferencesForm': called at the end of PreferencesForm::mainPrefsForm
 $form: the PreferencesForm
 $out: output page to render to, probably $wgOut
@@ -613,6 +857,10 @@ $namespace : Page namespace
 $title : Page title
 $text : Current text being indexed
 
+'SearchGetNearMatch': An extra chance for exact-title-matches in "go" searches
+$term : Search term string
+&$title : Outparam; set to $title object and return false for a match
+
 'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views
 $text: Text being shown
 $title: Title of the custom script/stylesheet page
@@ -632,6 +880,20 @@ $skin: Skin object
 &$text: bottomScripts Text
 Append to $text to add additional text/scripts after the stock bottom scripts.
 
+'SkinSubPageSubtitle': At the beginning of Skin::subPageSubtitle()
+$skin: Skin object
+&$subpages: Subpage links HTML
+If false is returned $subpages will be used instead of the HTML subPageSubtitle() generates.
+If true is returned, $subpages will be ignored and the rest of subPageSubtitle() will run.
+
+'SkinTemplateBuildContentActionUrlsAfterSpecialPage': after the single tab  when showing a special page
+$sktemplate: SkinTemplate object
+$content_actions: array of tabs
+
+'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink': after creating the "permanent link" tab
+$sktemplate: SkinTemplate object
+$nav_urls: array of tabs
+
 'SkinTemplateContentActions': Alter the "content action" links in SkinTemplates
 &$content_actions: Content actions
 [See http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/examples/Content_action.php
@@ -641,9 +903,64 @@ for an example]
 &$sktemplate: SkinTemplate object
 &$tpl: Template engine object
 
+'SkinTemplatePreventOtherActiveTabs': use this to prevent showing active tabs
+$sktemplate: SkinTemplate object
+$res: set to true to prevent active tabs
+
+'SkinTemplateSetupPageCss': use this to provide per-page CSS
+$out: Css to return
+
+'SkinTemplateTabAction': Override SkinTemplate::tabAction().
+       You can either create your own array, or alter the parameters for the normal one.
+&$this:     The SkinTemplate instance.
+$title:     Title instance for the page.
+$message:   Visible label of tab.
+$selected:  Whether this is a selected tab.
+$checkEdit: Whether or not the action=edit query should be added if appropriate.
+&$classes:  Array of CSS classes to apply.
+&$query:    Query string to add to link.
+&$text:     Link text.
+&$result:   Complete assoc. array if you want to return true.
+
+'SkinTemplateTabs': called when finished to build the actions tabs
+$sktemplate: SkinTemplate object
+$content_actions: array of tabs
+
 'SpecialContributionsBeforeMainOutput': Before the form on Special:Contributions
 $id: User identifier
 
+'SpecialMovepageAfterMove': called after moving a page
+$movePage: MovePageForm object
+$oldTitle: old title (object)
+$newTitle: new title (object)
+
+'SpecialPageExecuteAfterPage': called after executing a special page
+Warning: Not all the special pages call this hook
+$specialPage: SpecialPage object
+$par: paramter passed to the special page (string)
+$funct: function called to execute the special page
+
+'SpecialPageExecuteBeforeHeader': called before setting the header text of the special page
+Warning: Not all the special pages call this hook
+$specialPage: SpecialPage object
+$par: paramter passed to the special page (string)
+$funct: function called to execute the special page
+
+'SpecialPageExecuteBeforePage': called after setting the special page header text but before the main execution
+Warning: Not all the special pages call this hook
+$specialPage: SpecialPage object
+$par: paramter passed to the special page (string)
+$funct: function called to execute the special page
+
+'SpecialPage_initList': called when setting up SpecialPage::$mList, use this hook to remove a core special page
+$list: list (array) of core special pages
+
+'SpecialSearchNogomatch': called when user clicked the "Go" button but the target doesn't exist
+$title: title object generated from the text entred by the user
+
+'SpecialVersionExtensionTypes': called when generating the extensions credits, use this to change the tables headers
+$extTypes: associative array of extensions types
+
 'TitleMoveComplete': after moving an article (title)
 $old: old title
 $nt: new title
@@ -651,6 +968,10 @@ $user: user who did the move
 $pageid: database ID of the page that's been moved
 $redirid: database ID of the created redirect
 
+'UndeleteShowRevision': called when showing a revision in Special:Undelete
+$title: title object related to the revision
+$rev: revision (object) that will be viewed
+
 'UnknownAction': An unknown "action" has occured (useful for defining
 		 your own actions)
 $action: action name
@@ -687,16 +1008,29 @@ string &$error: output: HTML error to show if upload canceled by returning false
 'UploadComplete': Upon completion of a file upload
 $image: Image object representing the file that was uploaded
 
-'UserCan': To interrupt/advise the "user can do X to Y article" check
+'userCan': To interrupt/advise the "user can do X to Y article" check.
+	If you want to display an error message, try getUserPermissionsErrors.
 $title: Title object being checked against
 $user : Current user object
 $action: Action being checked
 $result: Pointer to result returned if hook returns false. If null is returned,
- UserCan checks are continued by internal code
+ userCan checks are continued by internal code.
+
+'UserCanSendEmail': To override User::canSendEmail() permission check
+$user: User (object) whose permission is being checked
+&$canSend: bool set on input, can override on output
+
+
+'UserClearNewTalkNotification': called when clearing the "You have new messages!" message, return false to not delete it
+$user: User (object) that'll clear the message
 
 'UserCreateForm': change to manipulate the login form
 $template: SimpleTemplate instance for the form
 
+'UserEffectiveGroups': Called in User::getEffectiveGroups()
+$user: User to get groups for
+&$groups: Current effective groups
+
 'UserLoginComplete': after a user has logged in
 $user: the user object that was created on login
 		    
@@ -721,9 +1055,12 @@ $remove: Array of strings corresponding to groups removed
 $user: User to get rights for
 &$rights: Current rights
 
-'UserEffectiveGroups': Called in User::getEffectiveGroups()
-$user: User to get groups for
-&$groups: Current effective groups
+'UserRetrieveNewTalks': called when retrieving "You have new messages!" message(s)
+$user: user retrieving new talks messages
+$talks: array of new talks page(s)
+
+'UserToggles': called when initialising User::$mToggles, use this to add new toggles
+$toggles: array of toggles to add
 
 'WatchArticle': before a watch is added to an article
 $user: user that will watch
@@ -733,6 +1070,8 @@ $article: article object to be watched
 $user: user that watched
 $article: article object watched
 
+'wgQueryPages': called when initialising $wgQueryPages, use this to add new query pages to be updated with maintenance/updateSpecialPages.php
+$query: $wgQueryPages itself
 
 More hooks might be available but undocumented, you can execute
-./maintenance/findhooks.php to find hidden one.
\ No newline at end of file
+./maintenance/findhooks.php to find hidden one.
diff --git a/extensions/LLAuthPlugin.php b/extensions/LLAuthPlugin.php
index 59bf19e7..f2aa7257 100644
--- a/extensions/LLAuthPlugin.php
+++ b/extensions/LLAuthPlugin.php
@@ -117,6 +117,10 @@ class LLAuthPlugin extends AuthPlugin {
 		return true;
 	}
 
+	function strictUserAuth( $username ) {
+		return true;
+	}
+
 	function initUser( $user, $autocreate=false ) {
 		$data = $this->getUserData($user->getName());
 		$user->setEmail($data['email']);
diff --git a/img_auth.php b/img_auth.php
index 2e8f6240..bb419b39 100644
--- a/img_auth.php
+++ b/img_auth.php
@@ -66,7 +66,7 @@ if( is_dir( $filename ) ) {
 
 // Stream the requested file
 wfDebugLog( 'img_auth', "Streaming `{$filename}`" );
-wfStreamFile( $filename );
+wfStreamFile( $filename, array( 'Cache-Control: private', 'Vary: Cookie' ) );
 wfLogProfilingData();
 
 /**
@@ -75,15 +75,16 @@ wfLogProfilingData();
  */
 function wfForbidden() {
 	header( 'HTTP/1.0 403 Forbidden' );
+	header( 'Vary: Cookie' );
 	header( 'Content-Type: text/html; charset=utf-8' );
-	echo <<<END
+	echo <<<ENDS
 <html>
 <body>
 <h1>Access Denied</h1>
 <p>You need to log in to access files on this server.</p>
 </body>
 </html>
-END;
+ENDS;
 	wfLogProfilingData();
 	exit();
-}
\ No newline at end of file
+}
diff --git a/img_auth.php5 b/img_auth.php5
index 2065de93..df25e951 100644
--- a/img_auth.php5
+++ b/img_auth.php5
@@ -1 +1 @@
-<?php require './img_auth.php'; ?>
+<?php require './img_auth.php';
\ No newline at end of file
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php
index 4fb76dcc..ffd3168a 100644
--- a/includes/AjaxFunctions.php
+++ b/includes/AjaxFunctions.php
@@ -73,64 +73,87 @@ function code2utf($num){
    return '';
 }
 
+define( 'AJAX_SEARCH_VERSION', 2 );	//AJAX search cache version
+
 function wfSajaxSearch( $term ) {
-	global $wgContLang, $wgOut;
+	global $wgContLang, $wgOut, $wgUser, $wgCapitalLinks, $wgMemc;
 	$limit = 16;
+	$sk = $wgUser->getSkin();
+	$output = '';
+
+	$term = trim( $term );
+	$term = $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) );
+	if ( $wgCapitalLinks )
+		$term = $wgContLang->ucfirst( $term ); 
+	$term_title = Title::newFromText( $term );
+
+	$memckey = $term_title ? wfMemcKey( 'ajaxsearch', md5( $term_title->getFullText() ) ) : wfMemcKey( 'ajaxsearch', md5( $term ) );
+	$cached = $wgMemc->get($memckey);
+	if( is_array( $cached ) && $cached['version'] == AJAX_SEARCH_VERSION ) {
+		$response = new AjaxResponse( $cached['html'] );
+		$response->setCacheDuration( 30*60 );
+		return $response;
+	}
 
-	$l = new Linker;
-
-	$term = str_replace( ' ', '_', $wgContLang->ucfirst( 
-			$wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) )
-		) );
-
-	if ( strlen( str_replace( '_', '', $term ) )<3 )
-		return;
-
-	$db = wfGetDB( DB_SLAVE );
-	$res = $db->select( 'page', 'page_title',
-			array(  'page_namespace' => 0,
-				"page_title LIKE '". $db->strencode( $term) ."%'" ),
-				"wfSajaxSearch",
-				array( 'LIMIT' => $limit+1 )
-			);
-
-	$r = "";
+	$r = $more = '';
+	$canSearch = true;
+	
+	$results = PrefixSearch::titleSearch( $term, $limit + 1 );
+	foreach( array_slice( $results, 0, $limit ) as $titleText ) {
+		$r .= '<li>' . $sk->makeKnownLink( $titleText ) . "</li>\n";
+	}
+	
+	// Hack to check for specials
+	if( $results ) {
+		$t = Title::newFromText( $results[0] );
+		if( $t && $t->getNamespace() == NS_SPECIAL ) {
+			$canSearch = false;
+			if( count( $results ) > $limit ) {
+				$more = '<i>' .
+					$sk->makeKnownLinkObj(
+						SpecialPage::getTitleFor( 'Specialpages' ),
+						wfMsgHtml( 'moredotdotdot' ) ) .
+					'</i>';
+			}
+		} else {
+			if( count( $results ) > $limit ) {
+				$more = '<i>' .
+					$sk->makeKnownLinkObj(
+						SpecialPage::getTitleFor( "Allpages", $term ),
+						wfMsgHtml( 'moredotdotdot' ) ) .
+					'</i>';
+			}
+		}
+	}
 
-	$i=0;
-	while ( ( $row = $db->fetchObject( $res ) ) && ( ++$i <= $limit ) ) {
-		$nt = Title::newFromDBkey( $row->page_title );
-		$r .= '<li>' . $l->makeKnownLinkObj( $nt ) . "</li>\n";
+	$valid = (bool) $term_title;
+	$term_url = urlencode( $term );
+	$term_diplay = htmlspecialchars( $valid ? $term_title->getFullText() : $term );
+	$subtitlemsg = ( $valid ? 'searchsubtitle' : 'searchsubtitleinvalid' );
+	$subtitle = wfMsgWikiHtml( $subtitlemsg, $term_diplay );
+	$html = '<div id="searchTargetHide"><a onclick="Searching_Hide_Results();">'
+		. wfMsgHtml( 'hideresults' ) . '</a></div>'
+		. '<h1 class="firstHeading">'.wfMsgHtml('search')
+		. '</h1><div id="contentSub">'. $subtitle . '</div>';
+	if( $canSearch ) {
+		$html .= '<ul><li>'
+			. $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ),
+						wfMsgHtml( 'searchcontaining', $term_diplay ),
+						"search={$term_url}&fulltext=Search" )
+			. '</li><li>' . $sk->makeKnownLink( $wgContLang->specialPage( 'Search' ),
+						wfMsgHtml( 'searchnamed', $term_diplay ) ,
+						"search={$term_url}&go=Go" )
+			. "</li></ul>";
 	}
-	if ( $i > $limit ) {
-		$more = '<i>' .  $l->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
-		                                wfMsg('moredotdotdot'),
-		                                "namespace=0&from=" . wfUrlEncode ( $term ) ) .
-			'</i>';
-	} else {
-		$more = '';
+	if( $r ) {
+		$html .= "<h2>" . wfMsgHtml( 'articletitles', $term_diplay ) . "</h2>"
+			. '<ul>' .$r .'</ul>' . $more;
 	}
 
-	$subtitlemsg = ( Title::newFromText($term) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
-	$subtitle = $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ); #FIXME: parser is missing mTitle !
-
-	$term = urlencode( $term );
-	$html = '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">'
-		. wfMsg( 'hideresults' ) . '</a></div>'
-		. '<h1 class="firstHeading">'.wfMsg('search')
-		. '</h1><div id="contentSub">'. $subtitle . '</div><ul><li>'
-		. $l->makeKnownLink( $wgContLang->specialPage( 'Search' ),
-					wfMsg( 'searchcontaining', $term ),
-					"search=$term&fulltext=Search" )
-		. '</li><li>' . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ),
-					wfMsg( 'searchnamed', $term ) ,
-					"search=$term&go=Go" )
-		. "</li></ul><h2>" . wfMsg( 'articletitles', $term ) . "</h2>"
-		. '<ul>' .$r .'</ul>'.$more;
+	$wgMemc->set( $memckey, array( 'version' => AJAX_SEARCH_VERSION, 'html' => $html ), 30 * 60 );
 
 	$response = new AjaxResponse( $html );
-
 	$response->setCacheDuration( 30*60 );
-
 	return $response;
 }
 
@@ -154,7 +177,7 @@ function wfAjaxWatch($pagename = "", $watch = "") {
 	}
 	$watch = 'w' === $watch;
 
-	$title = Title::newFromText($pagename);
+	$title = Title::newFromDBkey($pagename);
 	if(!$title) {
 		// Invalid title
 		return '<err#>';
diff --git a/includes/Article.php b/includes/Article.php
index 7ba55c54..0544db7d 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -36,24 +36,12 @@ class Article {
 	var $mUserText;			//!<
 	/**@}}*/
 
-	/**
-	 * Constants used by internal components to get rollback results
-	 */
-	const SUCCESS = 0;			// Operation successful
-	const PERM_DENIED = 1;		// Permission denied
-	const BLOCKED = 2;			// User has been blocked
-	const READONLY = 3;			// Wiki is in read-only mode
-	const BAD_TOKEN = 4;		// Invalid token specified
-	const BAD_TITLE = 5;		// $this is not a valid Article
-	const ALREADY_ROLLED = 6;	// Someone else already rolled this back. $from and $summary will be set
-	const ONLY_AUTHOR = 7;		// User is the only author of the page
- 
 	/**
 	 * Constructor and clear the article
 	 * @param $title Reference to a Title object.
 	 * @param $oldId Integer revision ID, null to fetch from request, zero for current
 	 */
-	function __construct( &$title, $oldId = null ) {
+	function __construct( Title $title, $oldId = null ) {
 		$this->mTitle =& $title;
 		$this->mOldId = $oldId;
 		$this->clear();
@@ -135,6 +123,7 @@ class Article {
 		$this->mRevIdFetched = 0;
 		$this->mRedirectUrl = false;
 		$this->mLatest = false;
+		$this->mPreparedEdit = false;
 	}
 
 	/**
@@ -622,8 +611,9 @@ class Article {
 	*/
 	function view()	{
 		global $wgUser, $wgOut, $wgRequest, $wgContLang;
-		global $wgEnableParserCache, $wgStylePath, $wgUseRCPatrol, $wgParser;
+		global $wgEnableParserCache, $wgStylePath, $wgParser;
 		global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies;
+		global $wgDefaultRobotPolicy;
 		$sk = $wgUser->getSkin();
 
 		wfProfileIn( __METHOD__ );
@@ -645,6 +635,7 @@ class Article {
 		$rcid = $wgRequest->getVal( 'rcid' );
 		$rdfrom = $wgRequest->getVal( 'rdfrom' );
 		$diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
+		$purge = $wgRequest->getVal( 'action' ) == 'purge';
 
 		$wgOut->setArticleFlag( true );
 
@@ -657,8 +648,7 @@ class Article {
 			# Honour customised robot policies for this namespace
 			$policy = $wgNamespaceRobotPolicies[$ns];
 		} else {
-			# Default to encourage indexing and following links
-			$policy = 'index,follow';
+			$policy = $wgDefaultRobotPolicy;
 		}
 		$wgOut->setRobotPolicy( $policy );
 
@@ -668,7 +658,7 @@ class Article {
 		if ( !is_null( $diff ) ) {
 			$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
 
-			$de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid );
+			$de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge );
 			// DifferenceEngine directly fetched the revision:
 			$this->mRevIdFetched = $de->mNewid;
 			$de->showDiffPage( $diffOnly );
@@ -780,11 +770,11 @@ class Article {
 					$this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid );
 					if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
 						if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
-							$wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+							$wgOut->addWikiMsg( 'rev-deleted-text-permission' );
 							$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
 							return;
 						} else {
-							$wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+							$wgOut->addWikiMsg( 'rev-deleted-text-view' );
 							// and we are allowed to see...
 						}
 					}
@@ -862,12 +852,12 @@ class Article {
 		# check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
 		if( $ns == NS_USER_TALK &&
 			User::isIP( $this->mTitle->getText() ) ) {
-			$wgOut->addWikiText( wfMsg('anontalkpagetext') );
+			$wgOut->addWikiMsg('anontalkpagetext');
 		}
 
 		# If we have been passed an &rcid= parameter, we want to give the user a
 		# chance to mark this new article as patrolled.
-		if ( $wgUseRCPatrol && !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) ) {
+		if( !is_null( $rcid ) && $rcid != 0 && $wgUser->isAllowed( 'patrol' ) && $this->mTitle->exists() ) {
 			$wgOut->addHTML(
 				"<div class='patrollink'>" .
 					wfMsgHtml( 'markaspatrolledlink',
@@ -914,31 +904,29 @@ class Article {
 					$o->tb_name,
 					$rmvtxt);
 		}
-		$wgOut->addWikitext(wfMsg('trackbackbox', $tbtext));
+		$wgOut->addWikiMsg( 'trackbackbox', $tbtext );
 	}
 
 	function deletetrackback() {
 		global $wgUser, $wgRequest, $wgOut, $wgTitle;
 
 		if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) {
-			$wgOut->addWikitext(wfMsg('sessionfailure'));
+			$wgOut->addWikiMsg( 'sessionfailure' );
 			return;
 		}
 
-		if ((!$wgUser->isAllowed('delete'))) {
-			$wgOut->permissionRequired( 'delete' );
-			return;
-		}
+		$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
 
-		if (wfReadOnly()) {
-			$wgOut->readOnlyPage();
+		if (count($permission_errors)>0)
+		{
+			$wgOut->showPermissionsErrorPage( $permission_errors );
 			return;
 		}
 
 		$db = wfGetDB(DB_MASTER);
 		$db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid')));
 		$wgTitle->invalidateCache();
-		$wgOut->addWikiText(wfMsg('trackbackdeleteok'));
+		$wgOut->addWikiMsg('trackbackdeleteok');
 	}
 
 	function render() {
@@ -960,7 +948,7 @@ class Article {
 			}
 		} else {
 			$msg = $wgOut->parse( wfMsg( 'confirm_purge' ) );
-			$action = $this->mTitle->escapeLocalURL( 'action=purge' );
+			$action = htmlspecialchars( $_SERVER['REQUEST_URI'] );
 			$button = htmlspecialchars( wfMsg( 'confirm_purge_button' ) );
 			$msg = str_replace( '$1',
 				"<form method=\"post\" action=\"$action\">\n" .
@@ -990,6 +978,15 @@ class Article {
 			$update = SquidUpdate::newSimplePurge( $this->mTitle );
 			$update->doUpdate();
 		}
+		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+			global $wgMessageCache;
+			if ( $this->getID() == 0 ) {
+				$text = false;
+			} else {
+				$text = $this->getContent();
+			}
+			$wgMessageCache->replace( $this->mTitle->getDBkey(), $text );
+		}
 		$this->view();
 	}
 
@@ -1200,10 +1197,11 @@ class Article {
 	/**
 	 * @deprecated use Article::doEdit()
 	 */
-	function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) {
+	function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) {
 		$flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 			( $isminor ? EDIT_MINOR : 0 ) |
-			( $suppressRC ? EDIT_SUPPRESS_RC : 0 );
+			( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) |
+			( $bot ? EDIT_FORCE_BOT : 0 );
 
 		# If this is a comment, add the summary as headline
 		if ( $comment && $summary != "" ) {
@@ -1322,7 +1320,7 @@ class Article {
 
 		# Silently ignore EDIT_MINOR if not allowed
 		$isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit');
-		$bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT );
+		$bot = $flags & EDIT_FORCE_BOT;
 
 		$oldtext = $this->getContent();
 		$oldsize = strlen( $oldtext );
@@ -1331,7 +1329,8 @@ class Article {
 		if ($flags & EDIT_AUTOSUMMARY && $summary == '')
 			$summary = $this->getAutosummary( $oldtext, $text, $flags );
 
-		$text = $this->preSaveTransform( $text );
+		$editInfo = $this->prepareTextForEdit( $text );
+		$text = $editInfo->pst;
 		$newsize = strlen( $text );
 
 		$dbw = wfGetDB( DB_MASTER );
@@ -1347,8 +1346,10 @@ class Article {
 
 			$lastRevision = 0;
 			$revisionId = 0;
+			
+			$changed = ( strcmp( $text, $oldtext ) != 0 );
 
-			if ( 0 != strcmp( $text, $oldtext ) ) {
+			if ( $changed ) {
 				$this->mGoodAdjustment = (int)$this->isCountable( $text )
 				  - (int)$this->isCountable( $oldtext );
 				$this->mTotalAdjustment = 0;
@@ -1413,9 +1414,8 @@ class Article {
 				# Invalidate cache of this article and all pages using this article
 				# as a template. Partly deferred.
 				Article::onArticleEdit( $this->mTitle );
-
+				
 				# Update links tables, site stats, etc.
-				$changed = ( strcmp( $oldtext, $text ) != 0 );
 				$this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed );
 			}
 		} else {
@@ -1451,7 +1451,7 @@ class Article {
 				$rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot,
 				  '', strlen( $text ), $revisionId );
 				# Mark as patrolled if the user can
-				if( $GLOBALS['wgUseRCPatrol'] && $wgUser->isAllowed( 'autopatrol' ) ) {
+				if( ($GLOBALS['wgUseRCPatrol'] || $GLOBALS['wgUseNPPatrol']) && $wgUser->isAllowed( 'autopatrol' ) ) {
 					RecentChange::markPatrolled( $rcid );
 					PatrolLog::record( $rcid, true );
 				}
@@ -1509,28 +1509,42 @@ class Article {
 	}
 
 	/**
-	 * Mark this particular edit as patrolled
+	 * Mark this particular edit/page as patrolled
 	 */
 	function markpatrolled() {
-		global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUser;
+		global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUseNPPatrol, $wgUser;
 		$wgOut->setRobotPolicy( 'noindex,nofollow' );
 
-		# Check RC patrol config. option
-		if( !$wgUseRCPatrol ) {
+		# Check patrol config options
+
+		if ( !($wgUseNPPatrol || $wgUseRCPatrol)) {
 			$wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
+			return;		
+		}
+
+		# If we haven't been given an rc_id value, we can't do anything
+		$rcid = (int) $wgRequest->getVal('rcid');
+		$rc = $rcid ? RecentChange::newFromId($rcid) : null;
+		if ( is_null ( $rc ) )
+		{
+			$wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
 			return;
 		}
 
-		# Check permissions
-		if( !$wgUser->isAllowed( 'patrol' ) ) {
-			$wgOut->permissionRequired( 'patrol' );
+		if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) {
+			// Only new pages can be patrolled if the general patrolling is off....???
+			// @fixme -- is this necessary? Shouldn't we only bother controlling the
+			// front end here?
+			$wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
 			return;
 		}
+		
+		# Check permissions
+		$permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser );
 
-		# If we haven't been given an rc_id value, we can't do anything
-		$rcid = $wgRequest->getVal( 'rcid' );
-		if( !$rcid ) {
-			$wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
+		if (count($permission_errors)>0)
+		{
+			$wgOut->showPermissionsErrorPage( $permission_errors );
 			return;
 		}
 
@@ -1539,7 +1553,10 @@ class Article {
 			return;
 		}
 
-		$return = SpecialPage::getTitleFor( 'Recentchanges' );
+		#It would be nice to see where the user had actually come from, but for now just guess
+		$returnto = $rc->mAttribs['rc_type'] == RC_NEW ? 'Newpages' : 'Recentchanges';
+		$return = Title::makeTitle( NS_SPECIAL, $returnto );
+
 		# If it's left up to us, check that the user is allowed to patrol this edit
 		# If the user has the "autopatrol" right, then we'll assume there are no
 		# other conditions stopping them doing so
@@ -1552,7 +1569,7 @@ class Article {
 				# The user made this edit, and can't patrol it
 				# Tell them so, and then back off
 				$wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
-				$wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrollederror-noautopatrol' ) );
+				$wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
 				$wgOut->returnToMain( false, $return );
 				return;
 			}
@@ -1565,7 +1582,7 @@ class Article {
 
 		# Inform the user
 		$wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) );
-		$wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) );
+		$wgOut->addWikiMsg( 'markedaspatrolledtext' );
 		$wgOut->returnToMain( false, $return );
 	}
 
@@ -1590,9 +1607,7 @@ class Article {
 			$wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
 			$wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-			$link = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
-			$text = wfMsg( 'addedwatchtext', $link );
-			$wgOut->addWikiText( $text );
+			$wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
 		}
 
 		$wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
@@ -1637,9 +1652,7 @@ class Article {
 			$wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
 			$wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-			$link = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
-			$text = wfMsg( 'removedwatchtext', $link );
-			$wgOut->addWikiText( $text );
+			$wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
 		}
 
 		$wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
@@ -1690,7 +1703,7 @@ class Article {
 		global $wgUser, $wgRestrictionTypes, $wgContLang;
 
 		$id = $this->mTitle->getArticleID();
-		if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) {
+		if( array() != $this->mTitle->getUserPermissionsErrors( 'protect', $wgUser ) || wfReadOnly() || $id == 0 ) {
 			return false;
 		}
 
@@ -1726,7 +1739,7 @@ class Article {
 
 				$expiry_description = '';
 				if ( $encodedExpiry != 'infinity' ) {
-					$expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')';
+					$expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ) ).')';
 				}
 
 				# Prepare a null revision to be added to the history
@@ -1757,10 +1770,8 @@ class Article {
 					$comment .= "$expiry_description";
 				if ( $cascade )
 					$comment .= "$cascade_description";
-
-				$nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
-				$nullRevId = $nullRevision->insertOn( $dbw );
-
+				
+				$rowsAffected = false;
 				# Update restrictions table
 				foreach( $limit as $action => $restrictions ) {
 					if ($restrictions != '' ) {
@@ -1768,11 +1779,22 @@ class Article {
 							array( 'pr_page' => $id, 'pr_type' => $action
 								, 'pr_level' => $restrictions, 'pr_cascade' => $cascade ? 1 : 0
 								, 'pr_expiry' => $encodedExpiry ), __METHOD__  );
+						if($dbw->affectedRows() != 0)
+							$rowsAffected = true;
 					} else {
 						$dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
 							'pr_type' => $action ), __METHOD__ );
+						if($dbw->affectedRows() != 0)
+							$rowsAffected = true;
 					}
 				}
+				if(!$rowsAffected)
+					// No change
+					return true;
+
+				# Insert a null revision
+				$nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
+				$nullRevId = $nullRevision->insertOn( $dbw );
 
 				# Update page record
 				$dbw->update( 'page',
@@ -1788,6 +1810,8 @@ class Article {
 
 				# Update the protection log
 				$log = new LogPage( 'protect' );
+				
+				
 
 				if( $protect ) {
 					$log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) );
@@ -1821,35 +1845,121 @@ class Article {
 		}
 		return implode( ':', $bits );
 	}
+	
+	/**
+	 * Auto-generates a deletion reason
+	 * @param bool &$hasHistory Whether the page has a history
+	 */
+	public function generateReason(&$hasHistory)
+	{
+		global $wgContLang;
+		$dbw = wfGetDB(DB_MASTER);
+		// Get the last revision
+		$rev = Revision::newFromTitle($this->mTitle);
+		if(is_null($rev))
+			return false;
+		// Get the article's contents
+		$contents = $rev->getText();
+		$blank = false;
+		// If the page is blank, use the text from the previous revision,
+		// which can only be blank if there's a move/import/protect dummy revision involved
+		if($contents == '')
+		{
+			$prev = $rev->getPrevious();
+			if($prev)
+			{
+				$contents = $prev->getText();
+				$blank = true;
+			}
+		}
+
+		// Find out if there was only one contributor
+		// Only scan the last 20 revisions
+		$limit = 20;
+		$res = $dbw->select('revision', 'rev_user_text', array('rev_page' => $this->getID()), __METHOD__,
+				array('LIMIT' => $limit));
+		if($res === false)
+			// This page has no revisions, which is very weird
+			return false;
+		if($res->numRows() > 1)
+				$hasHistory = true;
+		else
+				$hasHistory = false;
+		$row = $dbw->fetchObject($res);
+		$onlyAuthor = $row->rev_user_text;
+		// Try to find a second contributor
+		while( $row = $dbw->fetchObject($res) ) {
+			if($row->rev_user_text != $onlyAuthor) {
+				$onlyAuthor = false;
+				break;
+			}
+		}
+		$dbw->freeResult($res);
+
+		// Generate the summary with a '$1' placeholder
+		if($blank) {
+			// The current revision is blank and the one before is also
+			// blank. It's just not our lucky day
+			$reason = wfMsgForContent('exbeforeblank', '$1');
+		} else {
+			if($onlyAuthor)
+				$reason = wfMsgForContent('excontentauthor', '$1', $onlyAuthor);
+			else
+				$reason = wfMsgForContent('excontent', '$1');
+		}
+		
+		// Replace newlines with spaces to prevent uglyness
+		$contents = preg_replace("/[\n\r]/", ' ', $contents);
+		// Calculate the maximum amount of chars to get
+		// Max content length = max comment length - length of the comment (excl. $1) - '...'
+		$maxLength = 255 - (strlen($reason) - 2) - 3;
+		$contents = $wgContLang->truncate($contents, $maxLength, '...');
+		// Remove possible unfinished links
+		$contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
+		// Now replace the '$1' placeholder
+		$reason = str_replace( '$1', $contents, $reason );
+		return $reason;
+	}
+
 
 	/*
 	 * UI entry point for page deletion
 	 */
 	function delete() {
 		global $wgUser, $wgOut, $wgRequest;
+
 		$confirm = $wgRequest->wasPosted() &&
-			$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
-		$reason = $wgRequest->getText( 'wpReason' );
+				$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
+		
+		$this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
+		$this->DeleteReason = $wgRequest->getText( 'wpReason' );
+		
+		$reason = $this->DeleteReasonList;
+		
+		if ( $reason != 'other' && $this->DeleteReason != '') {
+			// Entry from drop down menu + additional comment
+			$reason .= ': ' . $this->DeleteReason;
+		} elseif ( $reason == 'other' ) {
+			$reason = $this->DeleteReason;
+		}
 
 		# This code desperately needs to be totally rewritten
 
-		# Check permissions
-		if( $wgUser->isAllowed( 'delete' ) ) {
-			if( $wgUser->isBlocked( !$confirm ) ) {
-				$wgOut->blockedPage();
-				return;
-			}
-		} else {
-			$wgOut->permissionRequired( 'delete' );
+		# Read-only check...
+		if ( wfReadOnly() ) {
+			$wgOut->readOnlyPage();
 			return;
 		}
+		
+		# Check permissions
+		$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
 
-		if( wfReadOnly() ) {
-			$wgOut->readOnlyPage();
+		if (count($permission_errors)>0) {
+			$wgOut->showPermissionsErrorPage( $permission_errors );
 			return;
 		}
 
-		$wgOut->setPagetitle( wfMsg( 'confirmdelete' ) );
+		$wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) );
 
 		# Better double-check that it hasn't been deleted yet!
 		$dbw = wfGetDB( DB_MASTER );
@@ -1860,6 +1970,15 @@ class Article {
 			return;
 		}
 
+		# Hack for big sites
+		$bigHistory = $this->isBigDeletion();
+		if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
+			global $wgLang, $wgDeleteRevisionsLimit;
+			$wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
+				array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+			return;
+		}
+
 		if( $confirm ) {
 			$this->doDelete( $reason );
 			if( $wgRequest->getCheck( 'wpWatch' ) ) {
@@ -1870,77 +1989,47 @@ class Article {
 			return;
 		}
 
-		# determine whether this page has earlier revisions
-		# and insert a warning if it does
-		$maxRevisions = 20;
-		$authors = $this->getLastNAuthors( $maxRevisions, $latest );
+		// Generate deletion reason
+		$hasHistory = false;
+		if ( !$reason ) $reason = $this->generateReason($hasHistory);
 
-		if( count( $authors ) > 1 && !$confirm ) {
+		// If the page has a history, insert a warning
+		if( $hasHistory && !$confirm ) {
 			$skin=$wgUser->getSkin();
 			$wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' );
-		}
-
-		# If a single user is responsible for all revisions, find out who they are
-		if ( count( $authors ) == $maxRevisions ) {
-			// Query bailed out, too many revisions to find out if they're all the same
-			$authorOfAll = false;
-		} else {
-			$authorOfAll = reset( $authors );
-			foreach ( $authors as $author ) {
-				if ( $authorOfAll != $author ) {
-					$authorOfAll = false;
-					break;
-				}
-			}
-		}
-		# Fetch article text
-		$rev = Revision::newFromTitle( $this->mTitle );
-
-		if( !is_null( $rev ) ) {
-			# if this is a mini-text, we can paste part of it into the deletion reason
-			$text = $rev->getText();
-
-			#if this is empty, an earlier revision may contain "useful" text
-			$blanked = false;
-			if( $text == '' ) {
-				$prev = $rev->getPrevious();
-				if( $prev ) {
-					$text = $prev->getText();
-					$blanked = true;
-				}
-			}
-
-			$length = strlen( $text );
-
-			# this should not happen, since it is not possible to store an empty, new
-			# page. Let's insert a standard text in case it does, though
-			if( $length == 0 && $reason === '' ) {
-				$reason = wfMsgForContent( 'exblank' );
-			}
-
-			if( $reason === '' ) {
-				# comment field=255, let's grep the first 150 to have some user
-				# space left
-				global $wgContLang;
-				$text = $wgContLang->truncate( $text, 150, '...' );
-
-				# let's strip out newlines
-				$text = preg_replace( "/[\n\r]/", '', $text );
-
-				if( !$blanked ) {
-					if( $authorOfAll === false ) {
-						$reason = wfMsgForContent( 'excontent', $text );
-					} else {
-						$reason = wfMsgForContent( 'excontentauthor', $text, $authorOfAll );
-					}
-				} else {
-					$reason = wfMsgForContent( 'exbeforeblank', $text );
-				}
+			if( $bigHistory ) {
+				global $wgLang, $wgDeleteRevisionsLimit;
+				$wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
+					array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
 			}
 		}
-
+		
 		return $this->confirmDelete( '', $reason );
 	}
+	
+	/**
+	 * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
+	 */
+	function isBigDeletion() {
+		global $wgDeleteRevisionsLimit;
+		if( $wgDeleteRevisionsLimit ) {
+			$revCount = $this->estimateRevisionCount();
+			return $revCount > $wgDeleteRevisionsLimit;
+		}
+		return false;
+	}
+	
+	/**
+	 * @return int approximate revision count
+	 */
+	function estimateRevisionCount() {
+		$dbr = wfGetDB();
+		// For an exact count...
+		//return $dbr->selectField( 'revision', 'COUNT(*)',
+		//	array( 'rev_page' => $this->getId() ), __METHOD__ );
+		return $dbr->estimateRowCount( 'revision', '*',
+		 	array( 'rev_page' => $this->getId() ), __METHOD__ );
+	}
 
 	/**
 	 * Get the last N authors
@@ -1990,51 +2079,59 @@ class Article {
 
 	/**
 	 * Output deletion confirmation dialog
+	 * @param $par string FIXME: do we need this parameter? One Call from Article::delete with '' only.
+	 * @param $reason string Prefilled reason
 	 */
 	function confirmDelete( $par, $reason ) {
-		global $wgOut, $wgUser;
+		global $wgOut, $wgUser, $wgContLang;
+		$align = $wgContLang->isRtl() ? 'left' : 'right';
 
 		wfDebug( "Article::confirmDelete\n" );
 
-		$sub = htmlspecialchars( $this->mTitle->getPrefixedText() );
-		$wgOut->setSubtitle( wfMsg( 'deletesub', $sub ) );
+		$wgOut->setSubtitle( wfMsg( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) );
 		$wgOut->setRobotpolicy( 'noindex,nofollow' );
-		$wgOut->addWikiText( wfMsg( 'confirmdeletetext' ) );
-
-		$formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par );
-
-		$confirm = htmlspecialchars( wfMsg( 'deletepage' ) );
-		$delcom = htmlspecialchars( wfMsg( 'deletecomment' ) );
-		$token = htmlspecialchars( $wgUser->editToken() );
-		$watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) );
-
-		$wgOut->addHTML( "
-<form id='deleteconfirm' method='post' action=\"{$formaction}\">
-	<table border='0'>
-		<tr>
-			<td align='right'>
-				<label for='wpReason'>{$delcom}:</label>
-			</td>
-			<td align='left'>
-				<input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" />
-			</td>
-		</tr>
-		<tr>
-			<td> </td>
-			<td>$watch</td>
-		</tr>
-		<tr>
-			<td> </td>
-			<td>
-				<input type='submit' name='wpConfirmB' id='wpConfirmB' value=\"{$confirm}\" tabindex=\"3\" />
-			</td>
-		</tr>
-	</table>
-	<input type='hidden' name='wpEditToken' value=\"{$token}\" />
-</form>\n" );
-
-		$wgOut->returnToMain( false );
-
+		$wgOut->addWikiMsg( 'confirmdeletetext' );
+
+		$form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) .
+			Xml::openElement( 'fieldset' ) .
+			Xml::element( 'legend', array(), wfMsg( 'delete-legend' ) ) .
+			Xml::openElement( 'table' ) .
+			"<tr id=\"wpDeleteReasonListRow\">
+				<td align='$align'>" .
+					Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
+				"</td>
+				<td>" .
+					Xml::listDropDown( 'wpDeleteReasonList',
+						wfMsgForContent( 'deletereason-dropdown' ), 
+						wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
+				"</td>
+			</tr>
+			<tr id=\"wpDeleteReasonRow\">
+				<td align='$align'>" .
+					Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
+				"</td>
+				<td>" .
+					Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
+				"</td>
+			</tr>
+			<tr>
+				<td></td>
+				<td>" .
+					Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) .
+				"</td>
+			</tr>
+			<tr>
+				<td></td>
+				<td>" .
+					Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) .
+				"</td>
+			</tr>" .
+			Xml::closeElement( 'table' ) .
+			Xml::closeElement( 'fieldset' ) .
+			Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+			Xml::closeElement( 'form' );
+
+		$wgOut->addHTML( $form );
 		$this->showLogExtract( $wgOut );
 	}
 
@@ -2062,15 +2159,14 @@ class Article {
 
 		if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) {
 			if ( $this->doDeleteArticle( $reason ) ) {
-				$deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+				$deleted = $this->mTitle->getPrefixedText();
 
 				$wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
 				$wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-				$loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
-				$text = wfMsg( 'deletedtext', $deleted, $loglink );
+				$loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
 
-				$wgOut->addWikiText( $text );
+				$wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
 				$wgOut->returnToMain( false );
 				wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason));
 			} else {
@@ -2180,50 +2276,76 @@ class Article {
 	/**
 	 * Roll back the most recent consecutive set of edits to a page
 	 * from the same user; fails if there are no eligible edits to
-	 * roll back to, e.g. user is the sole contributor
+	 * roll back to, e.g. user is the sole contributor. This function
+	 * performs permissions checks on $wgUser, then calls commitRollback()
+	 * to do the dirty work
 	 *
 	 * @param string $fromP - Name of the user whose edits to rollback. 
 	 * @param string $summary - Custom summary. Set to default summary if empty.
 	 * @param string $token - Rollback token.
-	 * @param bool $bot - If true, mark all reverted edits as bot.
+	 * @param bool   $bot - If true, mark all reverted edits as bot.
 	 * 
-	 * @param array $resultDetails contains result-specific dict of additional values
-	 *    ALREADY_ROLLED : 'current' (rev)
-	 *    SUCCESS        : 'summary' (str), 'current' (rev), 'target' (rev)
+	 * @param array $resultDetails contains result-specific array of additional values
+	 *    'alreadyrolled' : 'current' (rev)
+	 *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
 	 * 
-	 * @return self::SUCCESS on succes, self::* on failure
+	 * @return array of errors, each error formatted as
+	 *   array(messagekey, param1, param2, ...).
+	 * On success, the array is empty.  This array can also be passed to
+	 * OutputPage::showPermissionsErrorPage().
 	 */
 	public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
-		global $wgUser, $wgUseRCPatrol;
+		global $wgUser;
 		$resultDetails = null;
-		
-		if( $wgUser->isAllowed( 'rollback' ) ) {
-			if( $wgUser->isBlocked() ) {
-				return self::BLOCKED;
-			}
-		} else {
-			return self::PERM_DENIED;
-		}
-			
-		if ( wfReadOnly() ) {
-			return self::READONLY;
-		}
+
+		# Check permissions
+		$errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ),
+						$this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) );
 		if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
-			return self::BAD_TOKEN;
+			$errors[] = array( 'sessionfailure' );
 
+		if ( $wgUser->pingLimiter('rollback') || $wgUser->pingLimiter() ) {
+			$errors[] = array( 'actionthrottledtext' );
+		}
+		# If there were errors, bail out now
+		if(!empty($errors))
+			return $errors;
+		
+		return $this->commitRollback($fromP, $summary, $bot, $resultDetails);
+	}
+	
+	/**
+	 * Backend implementation of doRollback(), please refer there for parameter
+	 * and return value documentation
+	 *
+	 * NOTE: This function does NOT check ANY permissions, it just commits the
+	 * rollback to the DB Therefore, you should only call this function direct-
+	 * ly if you want to use custom permissions checks. If you don't, use
+	 * doRollback() instead.
+	 */	
+	public function commitRollback($fromP, $summary, $bot, &$resultDetails) {
+		global $wgUseRCPatrol, $wgUser;
 		$dbw = wfGetDB( DB_MASTER );
 
+		if( wfReadOnly() ) {
+			return array( array( 'readonlytext' ) );
+		}
+
 		# Get the last editor
 		$current = Revision::newFromTitle( $this->mTitle );
 		if( is_null( $current ) ) {
 			# Something wrong... no page?
-			return self::BAD_TITLE;
+			return array(array('notanarticle'));
 		}
 
 		$from = str_replace( '_', ' ', $fromP );
 		if( $from != $current->getUserText() ) {
 			$resultDetails = array( 'current' => $current );
-			return self::ALREADY_ROLLED;
+			return array(array('alreadyrolled',
+				htmlspecialchars($this->mTitle->getPrefixedText()),
+				htmlspecialchars($fromP),
+				htmlspecialchars($current->getUserText())
+			));
 		}
 
 		# Get the last edit not by this guy
@@ -2231,21 +2353,19 @@ class Article {
 		$user_text = $dbw->addQuotes( $current->getUserText() );
 		$s = $dbw->selectRow( 'revision',
 			array( 'rev_id', 'rev_timestamp' ),
-			array(
-				'rev_page' => $current->getPage(),
+			array(	'rev_page' => $current->getPage(),
 				"rev_user <> {$user} OR rev_user_text <> {$user_text}"
 			), __METHOD__,
-			array(
-				'USE INDEX' => 'page_timestamp',
+			array(	'USE INDEX' => 'page_timestamp',
 				'ORDER BY'  => 'rev_timestamp DESC' )
 			);
 		if( $s === false ) {
-			# Something wrong
-			return self::ONLY_AUTHOR;
+			# No one else ever edited this page
+			return array(array('cantrollback'));
 		}
 	
 		$set = array();
-		if ( $bot ) {
+		if ( $bot && $wgUser->isAllowed('markbotedits') ) {
 			# Mark all reverted edits as bot
 			$set['rc_bot'] = 1;
 		}
@@ -2264,23 +2384,36 @@ class Article {
 				);
 		}
 
-		# Get the edit summary
+		# Generate the edit summary if necessary
 		$target = Revision::newFromId( $s->rev_id );
 		if( empty( $summary ) )
-			$summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
+		{
+			global $wgLang;
+			$summary = wfMsgForContent( 'revertpage',
+					 $target->getUserText(), $from,
+					 $s->rev_id, $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true),
+					 $current->getId(), $wgLang->timeanddate($current->getTimestamp())
+			);
+		}
 
 		# Save
-		$flags = EDIT_UPDATE | EDIT_MINOR;
-		if( $bot )
+		$flags = EDIT_UPDATE;
+
+		if ($wgUser->isAllowed('minoredit'))
+			$flags |= EDIT_MINOR;
+
+		if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) )
 			$flags |= EDIT_FORCE_BOT;
 		$this->doEdit( $target->getText(), $summary, $flags );
 
+		wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) );
+
 		$resultDetails = array(
 			'summary' => $summary,
 			'current' => $current,
 			'target' => $target,
 		);
-		return self::SUCCESS;
+		return array();
 	}
 
 	/**
@@ -2288,8 +2421,8 @@ class Article {
 	 */
 	function rollback() {
 		global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
-
 		$details = null;
+
 		$result = $this->doRollback(
 			$wgRequest->getVal( 'from' ),
 			$wgRequest->getText( 'summary' ),
@@ -2298,58 +2431,44 @@ class Article {
 			$details
 		);
 
-		switch( $result ) {
-			case self::BLOCKED:
-				$wgOut->blockedPage();
-				break;
-			case self::PERM_DENIED:
-				$wgOut->permissionRequired( 'rollback' );
-				break;
-			case self::READONLY:
-				$wgOut->readOnlyPage( $this->getContent() );
-				break;
-			case self::BAD_TOKEN:
-				$wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
-				$wgOut->addWikiText( wfMsg( 'sessionfailure' ) );
-				break;
-			case self::BAD_TITLE:
-				$wgOut->addHtml( wfMsg( 'notanarticle' ) );
-				break;
-			case self::ALREADY_ROLLED:
-				$current = $details['current'];
-				$wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
-				$wgOut->addWikiText(
-					wfMsg( 'alreadyrolled',
-						htmlspecialchars( $this->mTitle->getPrefixedText() ),
-						htmlspecialchars( $wgRequest->getVal( 'from' ) ),
-						htmlspecialchars( $current->getUserText() )
-					)
-				);
-				if( $current->getComment() != '' ) {
-					$wgOut->addHtml( wfMsg( 'editcomment',
-						$wgUser->getSkin()->formatComment( $current->getComment() ) ) );
+		if( in_array( array( 'blocked' ), $result ) ) {
+			$wgOut->blockedPage();
+			return;
+		}
+		if( in_array( array( 'actionthrottledtext' ), $result ) ) {
+			$wgOut->rateLimited();
+			return;
+		}
+		# Display permissions errors before read-only message -- there's no
+		# point in misleading the user into thinking the inability to rollback
+		# is only temporary.
+		if( !empty($result) && $result !== array( array('readonlytext') ) ) {
+			# array_diff is completely broken for arrays of arrays, sigh.  Re-
+			# move any 'readonlytext' error manually.
+			$out = array();
+			foreach( $result as $error ) {
+				if( $error != array( 'readonlytext' ) ) {
+					$out []= $error;
 				}
-				break;
-			case self::ONLY_AUTHOR:
-				$wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
-				$wgOut->addHtml( wfMsg( 'cantrollback' ) );
-				break;
-			case self::SUCCESS:
-				$current = $details['current'];
-				$target = $details['target'];
-				$wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
-				$wgOut->setRobotPolicy( 'noindex,nofollow' );
-				$old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
-					. $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
-				$new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
-					. $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
-				$wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
-				$wgOut->returnToMain( false, $this->mTitle );
-				break;
-			default:
-				throw new MWException( __METHOD__ . ": Unknown return value `{$result}`" );
+			}
+			$wgOut->showPermissionsErrorPage( $out );
+			return;
+		}
+		if( $result == array( array('readonlytext') ) ) {
+			$wgOut->readOnlyPage();
+			return;
 		}
 
+		$current = $details['current'];
+		$target = $details['target'];
+		$wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
+		$wgOut->setRobotPolicy( 'noindex,nofollow' );
+		$old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
+			. $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
+		$new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
+			. $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
+		$wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
+		$wgOut->returnToMain( false, $this->mTitle );
 	}
 
 
@@ -2374,6 +2493,29 @@ class Article {
 		$wgUser->clearNotification( $this->mTitle );
 	}
 
+	/**
+	 * Prepare text which is about to be saved.
+	 * Returns a stdclass with source, pst and output members
+	 */
+	function prepareTextForEdit( $text, $revid=null ) {
+		if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) {
+			// Already prepared
+			return $this->mPreparedEdit;
+		}
+		global $wgParser;
+		$edit = (object)array();
+		$edit->revid = $revid;
+		$edit->newText = $text;
+		$edit->pst = $this->preSaveTransform( $text );
+		$options = new ParserOptions;
+		$options->setTidy( true );
+		$options->enableLimitReport();
+		$edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid );
+		$edit->oldText = $this->getContent();
+		$this->mPreparedEdit = $edit;
+		return $edit;
+	}
+
 	/**
 	 * Do standard deferred updates after page edit.
 	 * Update links tables, site stats, search index and message cache.
@@ -2388,21 +2530,28 @@ class Article {
 	 * @param $changed Whether or not the content actually changed
 	 */
 	function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
-		global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser;
+		global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache;
 
 		wfProfileIn( __METHOD__ );
 
 		# Parse the text
-		$options = new ParserOptions;
-		$options->setTidy(true);
-		$poutput = $wgParser->parse( $text, $this->mTitle, $options, true, true, $newid );
+		# Be careful not to double-PST: $text is usually already PST-ed once
+		if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
+			wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
+			$editInfo = $this->prepareTextForEdit( $text, $newid );
+		} else {
+			wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
+			$editInfo = $this->mPreparedEdit;
+		}
 
 		# Save it to the parser cache
-		$parserCache =& ParserCache::singleton();
-		$parserCache->save( $poutput, $this, $wgUser );
+		if ( $wgEnableParserCache ) {
+			$parserCache =& ParserCache::singleton();
+			$parserCache->save( $editInfo->output, $this, $wgUser );
+		}
 
 		# Update the links tables
-		$u = new LinksUpdate( $this->mTitle, $poutput );
+		$u = new LinksUpdate( $this->mTitle, $editInfo->output );
 		$u->doUpdate();
 
 		if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
@@ -2756,6 +2905,7 @@ class Article {
 
 		$title->touchLinks();
 		$title->purgeSquid();
+		$title->deleteTitleProtection();
 	}
 
 	static function onArticleDelete( $title ) {
@@ -2773,6 +2923,10 @@ class Article {
 		if( $title->getNamespace() == NS_MEDIAWIKI) {
 			$wgMessageCache->replace( $title->getDBkey(), false );
 		}
+		if( $title->getNamespace() == NS_IMAGE ) {
+			$update = new HTMLCacheUpdate( $title, 'imagelinks' );
+			$update->doUpdate();
+		}
 	}
 
 	/**
@@ -2782,9 +2936,11 @@ class Article {
 		global $wgDeferredUpdateList, $wgUseFileCache;
 
 		// Invalidate caches of articles which include this page
-		$update = new HTMLCacheUpdate( $title, 'templatelinks' );
-		$wgDeferredUpdateList[] = $update;
+		$wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' );
 
+		// Invalidate the caches of all pages which redirect here
+		$wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' );
+		
 		# Purge squid for this page only
 		$title->purgeSquid();
 
@@ -3009,14 +3165,16 @@ class Article {
 	 * @param bool    $cache
 	 */
 	public function outputWikiText( $text, $cache = true ) {
-		global $wgParser, $wgUser, $wgOut;
+		global $wgParser, $wgUser, $wgOut, $wgEnableParserCache;
 
 		$popts = $wgOut->parserOptions();
 		$popts->setTidy(true);
+		$popts->enableLimitReport();
 		$parserOutput = $wgParser->parse( $text, $this->mTitle,
 			$popts, true, true, $this->getRevIdFetched() );
 		$popts->setTidy(false);
-		if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) {
+		$popts->enableLimitReport( false );
+		if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) {
 			$parserCache =& ParserCache::singleton();
 			$parserCache->save( $parserOutput, $this, $wgUser );
 		}
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index 87a79438..2ad137e2 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -28,10 +28,6 @@
  * accounts authenticate externally, or use it only as a fallback; also
  * you can transparently create internal wiki accounts the first time
  * someone logs in who can be authenticated externally.
- *
- * This interface is new, and might change a bit before 1.4.0 final is
- * done...
- *
  */
 class AuthPlugin {
 	/**
@@ -210,6 +206,18 @@ class AuthPlugin {
 		return false;
 	}
 
+	/**
+	 * Check if a user should authenticate locally if the global authentication fails.
+	 * If either this or strict() returns true, local authentication is not used.
+	 *
+	 * @param $username String: username.
+	 * @return bool
+	 * @public
+	 */
+	function strictUserAuth( $username ) {
+		return false;
+	}
+
 	/**
 	 * When creating a user account, optionally fill in preferences and such.
 	 * For instance, you might pull the email address or real name from the
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 5e1b8156..2e2083b2 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -7,6 +7,8 @@ ini_set('unserialize_callback_func', '__autoload' );
 function __autoload($className) {
 	global $wgAutoloadClasses;
 
+	# Locations of core classes
+	# Extension classes are specified with $wgAutoloadClasses
 	static $localClasses = array(
 		# Includes
 		'AjaxDispatcher' => 'includes/AjaxDispatcher.php',
@@ -15,6 +17,7 @@ function __autoload($className) {
 		'AlphabeticPager' => 'includes/Pager.php',
 		'Article' => 'includes/Article.php',
 		'AuthPlugin' => 'includes/AuthPlugin.php',
+		'Autopromote' => 'includes/Autopromote.php',
 		'BagOStuff' => 'includes/BagOStuff.php',
 		'HashBagOStuff' => 'includes/BagOStuff.php',
 		'SqlBagOStuff' => 'includes/BagOStuff.php',
@@ -55,6 +58,8 @@ function __autoload($className) {
 		'Diff' => 'includes/DifferenceEngine.php',
 		'MappedDiff' => 'includes/DifferenceEngine.php',
 		'DiffFormatter' => 'includes/DifferenceEngine.php',
+		'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php',
+		'ArrayDiffFormatter' => 'includes/DifferenceEngine.php',
 		'DjVuImage' => 'includes/DjVuImage.php',
 		'_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php',
 		'WordLevelDiff' => 'includes/DifferenceEngine.php',
@@ -88,7 +93,6 @@ function __autoload($className) {
 		'FileStore' => 'includes/FileStore.php',
 		'FSException' => 'includes/FileStore.php',
 		'FSTransaction' => 'includes/FileStore.php',
-		'HTMLForm' => 'includes/HTMLForm.php',
 		'HistoryBlob' => 'includes/HistoryBlob.php',
 		'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
 		'HistoryBlobStub' => 'includes/HistoryBlob.php',
@@ -99,7 +103,6 @@ function __autoload($className) {
 		'ImageGallery' => 'includes/ImageGallery.php',
 		'ImagePage' => 'includes/ImagePage.php',
 		'ImageHistoryList' => 'includes/ImagePage.php',
-		'ImageRemote' => 'includes/ImageRemote.php',
 		'FileDeleteForm' => 'includes/FileDeleteForm.php',
 		'FileRevertForm' => 'includes/FileRevertForm.php',
 		'Job' => 'includes/JobQueue.php',
@@ -134,10 +137,23 @@ function __autoload($className) {
 		'ReverseChronologicalPager' => 'includes/Pager.php',
 		'TablePager' => 'includes/Pager.php',
 		'Parser' => 'includes/Parser.php',
+		'Parser_OldPP' => 'includes/Parser_OldPP.php',
+		'Parser_DiffTest' => 'includes/Parser_DiffTest.php',
+		'ParserCache' => 'includes/ParserCache.php',
 		'ParserOutput' => 'includes/ParserOutput.php',
 		'ParserOptions' => 'includes/ParserOptions.php',
-		'ParserCache' => 'includes/ParserCache.php',
 		'PatrolLog' => 'includes/PatrolLog.php',
+		'Preprocessor' => 'includes/Preprocessor.php',
+		'PrefixSearch' => 'includes/PrefixSearch.php',
+		'PPFrame' => 'includes/Preprocessor.php',
+		'PPNode' => 'includes/Preprocessor.php',
+		'Preprocessor_DOM' => 'includes/Preprocessor_DOM.php',
+		'PPFrame_DOM' => 'includes/Preprocessor_DOM.php',
+		'PPTemplateFrame_DOM' => 'includes/Preprocessor_DOM.php',
+		'PPDStack' => 'includes/Preprocessor_DOM.php',
+		'PPDStackElement' => 'includes/Preprocessor_DOM.php',
+		'PPNode_DOM' => 'includes/Preprocessor_DOM.php',
+		'Preprocessor_Hash' => 'includes/Preprocessor_Hash.php',
 		'ProfilerSimple' => 'includes/ProfilerSimple.php',
 		'ProfilerSimpleUDP' => 'includes/ProfilerSimpleUDP.php',
 		'Profiler' => 'includes/Profiler.php',
@@ -207,6 +223,8 @@ function __autoload($className) {
 		'PopularPagesPage' => 'includes/SpecialPopularpages.php',
 		'PreferencesForm' => 'includes/SpecialPreferences.php',
 		'SpecialPrefixindex' => 'includes/SpecialPrefixindex.php',
+		'RandomPage' => 'includes/SpecialRandompage.php',
+		'SpecialRandomredirect' => 'includes/SpecialRandomredirect.php',
 		'PasswordResetForm' => 'includes/SpecialResetpass.php',
 		'RevisionDeleteForm' => 'includes/SpecialRevisiondelete.php',
 		'RevisionDeleter' => 'includes/SpecialRevisiondelete.php',
@@ -225,7 +243,7 @@ function __autoload($className) {
 		'UploadForm' => 'includes/SpecialUpload.php',
 		'UploadFormMogile' => 'includes/SpecialUploadMogile.php',
 		'LoginForm' => 'includes/SpecialUserlogin.php',
-		'UserrightsForm' => 'includes/SpecialUserrights.php',
+		'UserrightsPage' => 'includes/SpecialUserrights.php',
 		'SpecialVersion' => 'includes/SpecialVersion.php',
 		'WantedCategoriesPage' => 'includes/SpecialWantedcategories.php',
 		'WantedPagesPage' => 'includes/SpecialWantedpages.php',
@@ -240,8 +258,10 @@ function __autoload($className) {
 		'StringUtils' => 'includes/StringUtils.php',
 		'Title' => 'includes/Title.php',
 		'User' => 'includes/User.php',
+		'UserRightsProxy' => 'includes/UserRightsProxy.php',
 		'MailAddress' => 'includes/UserMailer.php',
 		'EmailNotification' => 'includes/UserMailer.php',
+		'UserMailer' => 'includes/UserMailer.php',
 		'WatchedItem' => 'includes/WatchedItem.php',
 		'WebRequest' => 'includes/WebRequest.php',
 		'WebResponse' => 'includes/WebResponse.php',
@@ -251,6 +271,7 @@ function __autoload($className) {
 		'WikiErrorMsg' => 'includes/WikiError.php',
 		'WikiXmlError' => 'includes/WikiError.php',
 		'Xml' => 'includes/Xml.php',
+		'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
 		'ZhClient' => 'includes/ZhClient.php',
 		'memcached' => 'includes/memcached-client.php',
 		'EmaillingJob' => 'includes/JobQueue.php',
@@ -290,10 +311,10 @@ function __autoload($className) {
 
 		# Languages
 		'Language' => 'languages/Language.php',
-		'RandomPage' => 'includes/SpecialRandompage.php',
 
 		# API
 		'ApiBase' => 'includes/api/ApiBase.php',
+		'ApiExpandTemplates' => 'includes/api/ApiExpandTemplates.php',
 		'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
 		'ApiFeedWatchlist' => 'includes/api/ApiFeedWatchlist.php',
 		'ApiFormatBase' => 'includes/api/ApiFormatBase.php',
@@ -302,16 +323,22 @@ function __autoload($className) {
 		'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
 		'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php',
 		'ApiFormatXml' => 'includes/api/ApiFormatXml.php',
+		'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
+		'ApiFormatDbg' => 'includes/api/ApiFormatDbg.php',
 		'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
 		'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php',
 		'ApiHelp' => 'includes/api/ApiHelp.php',
 		'ApiLogin' => 'includes/api/ApiLogin.php',
+		'ApiLogout' => 'includes/api/ApiLogout.php',
 		'ApiMain' => 'includes/api/ApiMain.php',
 		'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
 		'ApiPageSet' => 'includes/api/ApiPageSet.php',
+		'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
+		'ApiParse' => 'includes/api/ApiParse.php',
 		'ApiQuery' => 'includes/api/ApiQuery.php',
 		'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php',
 		'ApiQueryAllLinks' => 'includes/api/ApiQueryAllLinks.php',
+		'ApiQueryAllCategories' => 'includes/api/ApiQueryAllCategories.php',
 		'ApiQueryAllUsers' => 'includes/api/ApiQueryAllUsers.php',
 		'ApiQueryBase' => 'includes/api/ApiQueryBase.php',
 		'ApiQueryGeneratorBase' => 'includes/api/ApiQueryBase.php',
@@ -327,13 +354,29 @@ function __autoload($className) {
 		'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php',
 		'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php',
 		'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php',
+		'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php',
 		'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php',
 		'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
 		'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
+		'ApiQueryAllmessages' => 'includes/api/ApiQueryAllmessages.php',
 		'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
+		'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
 		'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php',
 		'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
 		'ApiResult' => 'includes/api/ApiResult.php',
+
+		# apiedit branch
+		'ApiBlock' => 'includes/api/ApiBlock.php',
+		#'ApiChangeRights' => 'includes/api/ApiChangeRights.php',
+		# Disabled for now
+		'ApiDelete' => 'includes/api/ApiDelete.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'
 	);
 	
 	wfProfileIn( __METHOD__ );
@@ -383,4 +426,4 @@ function wfLoadAllExtensions() {
 			require( $file );
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/includes/Autopromote.php b/includes/Autopromote.php
new file mode 100644
index 00000000..b5097423
--- /dev/null
+++ b/includes/Autopromote.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * This class checks if user can get extra rights
+ * because of conditions specified in $wgAutopromote
+ */
+class Autopromote {
+	/**
+	 * Get the groups for the given user based on $wgAutopromote.
+	 *
+	 * @param User $user The user to get the groups for
+	 * @return array Array of groups to promote to.
+	 */
+	public static function getAutopromoteGroups( User $user ) {
+		global $wgAutopromote;
+		$promote = array();
+		foreach( $wgAutopromote as $group => $cond ) {
+			if( self::recCheckCondition( $cond, $user ) )
+				$promote[] = $group;
+		}
+		return $promote;
+	}
+
+	/**
+	 * Recursively check a condition.  Conditions are in the form
+	 *   array( '&' or '|' or '^', cond1, cond2, ... )
+	 * where cond1, cond2, ... are themselves conditions; *OR*
+	 *   APCOND_EMAILCONFIRMED, *OR*
+	 *   array( APCOND_EMAILCONFIRMED ), *OR*
+	 *   array( APCOND_EDITCOUNT, number of edits ), *OR*
+	 *   array( APCOND_AGE, seconds since registration ), *OR*
+	 *   similar constructs defined by extensions.
+	 * This function evaluates the former type recursively, and passes off to
+	 * self::checkCondition for evaluation of the latter type.
+	 *
+	 * @param mixed $cond A condition, possibly containing other conditions
+	 * @param User  $user The user to check the conditions against
+	 * @return bool Whether the condition is true
+	 */
+	private static function recCheckCondition( $cond, User $user ) {
+		$validOps = array( '&', '|', '^' );
+		if( is_array( $cond ) && count( $cond ) >= 2 && in_array( $cond[0], $validOps ) ) {
+			# Recursive condition
+			if( $cond[0] == '&' ) {
+				foreach( array_slice( $cond, 1 ) as $subcond )
+					if( !self::recCheckCondition( $subcond, $user ) )
+						return false;
+				return true;
+			} elseif( $cond[0] == '|' ) {
+				foreach( array_slice( $cond, 1 ) as $subcond ) 
+					if( self::recCheckCondition( $subcond, $user ) )
+						return true;
+				return false;
+			} elseif( $cond[0] == '^' ) {
+				$res = null;
+				foreach( array_slice( $cond, 1 ) as $subcond ) {
+					if( is_null( $res ) )
+						$res = self::recCheckCondition( $subcond, $user );
+					else
+						$res = ($res xor self::recCheckCondition( $subcond, $user ));
+				}
+				return $res;
+			}
+		}
+		# If we got here, the array presumably does not contain other condi-
+		# tions; it's not recursive.  Pass it off to self::checkCondition.
+		if( !is_array( $cond ) )
+			$cond = array( $cond );
+		return self::checkCondition( $cond, $user );
+	}
+
+	/**
+	 * As recCheckCondition, but *not* recursive.  The only valid conditions
+	 * are those whose first element is APCOND_EMAILCONFIRMED/APCOND_EDITCOUNT/
+	 * APCOND_AGE.  Other types will throw an exception if no extension evalu-
+	 * ates them.
+	 *
+	 * @param array $cond A condition, which must not contain other conditions
+	 * @param User  $user The user to check the condition against
+	 * @return bool Whether the condition is true for the user
+	 */
+	private static function checkCondition( $cond, User $user ) {
+		if( count( $cond ) < 1 )
+			return false;
+		switch( $cond[0] ) {
+			case APCOND_EMAILCONFIRMED:
+				if( User::isValidEmailAddr( $user->getEmail() ) ) {
+					global $wgEmailAuthentication;
+					if( $wgEmailAuthentication ) {
+						return $user->getEmailAuthenticationTimestamp() ? true : false;
+					} else {
+						return true;
+					}
+				}
+				return false;
+			case APCOND_EDITCOUNT:
+				return $user->getEditCount() >= $cond[1];
+			case APCOND_AGE:
+				$age = time() - wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
+				return $age >= $cond[1];
+			case APCOND_INGROUPS:
+				$groups = array_slice( $cond, 1 );
+				return count( array_intersect( $groups, $user->getGroups() ) ) == count( $groups );
+			default:
+				$result = null;
+				wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) );
+				if( $result === null ) {
+					throw new MWException( "Unrecognized condition {$cond[0]} for autopromotion!" );
+				}
+				return $result ? true : false;
+		}
+	}
+}
diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php
index a40d020e..226abb35 100644
--- a/includes/BagOStuff.php
+++ b/includes/BagOStuff.php
@@ -73,6 +73,11 @@ class BagOStuff {
 		return true;
 	}
 
+	function keys() {
+		/* stub */
+		return array();
+	}
+
 	/* *** Emulated functions *** */
 	/* Better performance can likely be got with custom written versions */
 	function get_multi($keys) {
@@ -202,6 +207,10 @@ class HashBagOStuff extends BagOStuff {
 		unset($this->bag[$key]);
 		return true;
 	}
+
+	function keys() {
+		return array_keys( $this->bag );
+	}
 }
 
 /*
@@ -283,6 +292,19 @@ abstract class SqlBagOStuff extends BagOStuff {
 		return true; /* ? */
 	}
 
+	function keys() {
+		$res = $this->_query( "SELECT keyname FROM $0" );
+		if(!$res) {
+			$this->_debug("keys: ** error: " . $this->_dberror($res) . " **");
+			return array();
+		}
+		$result = array();
+		while( $row = $this->_fetchobject($res) ) {
+			$result[] = $row->keyname;
+		}
+		return $result;
+	}
+
 	function getTableName() {
 		return $this->table;
 	}
@@ -743,6 +765,19 @@ class DBABagOStuff extends BagOStuff {
 		wfProfileOut( __METHOD__ );
 		return $ret;
 	}
+
+	function keys() {
+		$reader = $this->getReader();
+		$k1 = dba_firstkey( $reader );
+		if( !$k1 ) {
+			return array();
+		}
+		$result[] = $k1;
+		while( $key = dba_nextkey( $reader ) ) {
+			$result[] = $key;
+		}
+		return $result;
+	}
 }
 	
 
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index 76a388a6..6fbcd3c1 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -37,6 +37,19 @@ class CategoryPage extends Article {
 		}
 	}
 
+	/**
+	 * This page should not be cached if 'from' or 'until' has been used
+	 * @return bool
+	 */
+	function isFileCacheable() {
+		global $wgRequest;
+
+		return ( ! Article::isFileCacheable()
+				|| $wgRequest->getVal( 'from' )
+				|| $wgRequest->getVal( 'until' )
+		) ? false : true;
+	}
+
 	function openShowCategory() {
 		# For overloading
 	}
@@ -202,7 +215,7 @@ class CategoryViewer {
 			array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey' ),
 			array( $pageCondition,
 			       'cl_from          =  page_id',
-			       'cl_to'           => $this->title->getDBKey()),
+			       'cl_to'           => $this->title->getDBkey()),
 			       #'page_is_redirect' => 0),
 			#+ $pageCondition,
 			__METHOD__,
@@ -410,7 +423,7 @@ class CategoryViewer {
 	 * @private
 	 */
 	function pagingLinks( $title, $first, $last, $limit, $query = array() ) {
-		global $wgUser, $wgLang;
+		global $wgLang;
 		$sk = $this->getSkin();
 		$limitText = $wgLang->formatNum( $limit );
 
diff --git a/includes/ChangesList.php b/includes/ChangesList.php
index 8d0f9508..507e88fa 100644
--- a/includes/ChangesList.php
+++ b/includes/ChangesList.php
@@ -58,7 +58,7 @@ class ChangesList {
 		// Precache various messages
 		if( !isset( $this->message ) ) {
 			foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '.
-				'blocklink history boteditletter' ) as $msg ) {
+				'blocklink history boteditletter semicolon-separator' ) as $msg ) {
 				$this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
 			}
 		}
@@ -176,13 +176,16 @@ class ChangesList {
 		global $wgContLang;
 		$articlelink .= $wgContLang->getDirMark();
 
+		wfRunHooks('ChangesListInsertArticleLink',
+			array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched));
+		
 		$s .= ' '.$articlelink;
 	}
 
 	function insertTimestamp(&$s, $rc) {
 		global $wgLang;
 		# Timestamp
-		$s .= '; ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . ';
+		$s .= $this->message['semicolon-separator'] . ' ' . $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . ';
 	}
 
 	/** Insert links to user page, user talk page and eventually a blocking link */
@@ -453,7 +456,7 @@ class EnhancedChangesList extends ChangesList {
 			array_push( $users, $text );
 		}
 
-		$users = ' <span class="changedby">['.implode('; ',$users).']</span>';
+		$users = ' <span class="changedby">[' . implode( $this->message['semicolon-separator'] . ' ', $users ) . ']</span>';
 
 		# Arrow
 		$rci = 'RCI'.$this->rcCacheIndex;
@@ -546,7 +549,7 @@ class EnhancedChangesList extends ChangesList {
 			$r .= $link;
 			$r .= ' (';
 			$r .= $rcObj->curlink;
-			$r .= '; ';
+			$r .= $this->message['semicolon-separator'] . ' ';
 			$r .= $rcObj->lastlink;
 			$r .= ') . . ';
 
@@ -651,7 +654,7 @@ class EnhancedChangesList extends ChangesList {
 		$r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
 
 		# Diff
-		$r .= ' ('. $rcObj->difflink .'; ';
+		$r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'] . ' ';
 
 		# Hist
 		$r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . ';
@@ -704,4 +707,3 @@ class EnhancedChangesList extends ChangesList {
 	}
 
 }
-
diff --git a/includes/CoreParserFunctions.php b/includes/CoreParserFunctions.php
index a5f45016..61dbafe5 100644
--- a/includes/CoreParserFunctions.php
+++ b/includes/CoreParserFunctions.php
@@ -51,12 +51,20 @@ class CoreParserFunctions {
 
 	static function lc( $parser, $s = '' ) {
 		global $wgContLang;
-		return $wgContLang->lc( $s );
+		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;
-		return $wgContLang->uc( $s );
+		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 ); }
@@ -92,9 +100,10 @@ class CoreParserFunctions {
 		return $parser->getFunctionLang()->convertGrammar( $word, $case );
 	}
 
-	static function plural( $parser, $text = '', $arg0 = null, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null ) {
+	static function plural( $parser, $text = '') {
+		$forms = array_slice( func_get_args(), 2);
 		$text = $parser->getFunctionLang()->parseFormattedNumber( $text );
-		return $parser->getFunctionLang()->convertPlural( $text, $arg0, $arg1, $arg2, $arg3, $arg4 );
+		return $parser->getFunctionLang()->convertPlural( $text, $forms );
 	}
 
 	/**
@@ -190,12 +199,70 @@ class CoreParserFunctions {
 			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
index 4f8c7d5e..f8738288 100644
--- a/includes/Database.php
+++ b/includes/Database.php
@@ -35,6 +35,22 @@ class DBObject {
 	}
 };
 
+/**
+ * Utility class
+ * @addtogroup 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.
  * @addtogroup Database
@@ -729,8 +745,8 @@ class Database {
 			global $wgUser;
 			if ( is_object( $wgUser ) && !($wgUser instanceof StubObject) ) {
 				$userName = $wgUser->getName();
-				if ( strlen( $userName ) > 15 ) {
-					$userName = substr( $userName, 0, 15 ) . '...';
+				if ( mb_strlen( $userName ) > 15 ) {
+					$userName = mb_substr( $userName, 0, 15 ) . '...';
 				}
 				$userName = str_replace( '/', '', $userName );
 			} else {
@@ -743,9 +759,13 @@ class Database {
 
 		# If DBO_TRX is set, start a transaction
 		if ( ( $this->mFlags & DBO_TRX ) && !$this->trxLevel() && 
-			$sql != 'BEGIN' && $sql != 'COMMIT' && $sql != 'ROLLBACK' 
-		) {
-			$this->begin();
+			$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() ) {
@@ -1548,7 +1568,15 @@ class Database {
 			} elseif ( ($mode == LIST_SET) && is_numeric( $field ) ) {
 				$list .= "$value";
 			} elseif ( ($mode == LIST_AND || $mode == LIST_OR) && is_array($value) ) {
-				$list .= $field." IN (".$this->makeList($value).") ";
+				if( count( $value ) == 0 ) {
+					// Empty input... or should this throw an error?
+					$list .= '0';
+				} elseif( count( $value ) == 1 ) {
+					// Special-case single values, as IN isn't terribly efficient
+					$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 ";
@@ -2011,10 +2039,11 @@ class Database {
 	}
 
 	/**
-	 * Rollback a transaction
+	 * Rollback a transaction.
+	 * No-op on non-transactional databases.
 	 */
 	function rollback( $fname = 'Database::rollback' ) {
-		$this->query( 'ROLLBACK', $fname );
+		$this->query( 'ROLLBACK', $fname, true );
 		$this->mTrxLevel = 0;
 	}
 
@@ -2286,6 +2315,13 @@ class Database {
 		return $this->tableName( $matches[1] );
 	}
 
+	/*
+	 * Build a concatenation list to feed into a SQL query
+	*/
+	function buildConcat( $stringList ) {
+		return 'CONCAT(' . implode( ',', $stringList ) . ')';
+	}
+
 }
 
 /**
diff --git a/includes/DatabasePostgres.php b/includes/DatabasePostgres.php
index 32c061a0..01213715 100644
--- a/includes/DatabasePostgres.php
+++ b/includes/DatabasePostgres.php
@@ -16,7 +16,13 @@ class PostgresField {
 	global $wgDBmwschema;
 
 		$q = <<<END
-SELECT typname, attnotnull, attlen
+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'
@@ -112,6 +118,12 @@ class DatabasePostgres extends Database {
 		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 );
@@ -135,7 +147,7 @@ class DatabasePostgres extends Database {
 
 		$this->close();
 		$this->mServer = $server;
-		$port = $wgDBport;
+		$this->mPort = $port = $wgDBport;
 		$this->mUser = $user;
 		$this->mPassword = $password;
 		$this->mDBname = $dbName;
@@ -148,7 +160,6 @@ class DatabasePostgres extends Database {
 			$hstring .= "port=$port ";
 		}
 
-
 		error_reporting( E_ALL );
 		@$this->mConn = pg_connect("$hstring dbname=$dbName user=$user password=$password");
 
@@ -160,87 +171,117 @@ class DatabasePostgres extends Database {
 		}
 
 		$this->mOpened = true;
-		## If this is the initial connection, setup the schema stuff and possibly create the user
-		## TODO: Move this out of open()
-		if (defined('MEDIAWIKI_INSTALL')) {
-			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>");
+
+		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>');
 			}
-			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";
+			$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>');
 				}
-				$perms = pg_fetch_result($res, 0, 0);
-
-				$SQL = "SELECT 1 FROM pg_catalog.pg_user WHERE usename = " . $this->addQuotes($wgDBuser);
+				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>User \"$wgDBuser\" already exists, skipping account creation.</li>";
+					print "<li>Database \"$wgDBname\" already exists, skipping database creation.</li>";
 				}
 				else {
-					if ($perms != 1 and $perms != 3) {
-						print "<li>ERROR: the user \"$wgDBsuperuser\" cannot create other users. ";
+					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 user <b>$wgDBuser</b>...";
-					$safepass = $this->addQuotes($wgDBpassword);
-					$SQL = "CREATE USER $safeuser NOCREATEDB PASSWORD $safepass";
+					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...
 				}
-				## 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...";
-					@$this->mConn = pg_connect("$hstring dbname=$wgDBname user=$user password=$password");
-					if ( $this->mConn == false ) {
-						print "<b>FAILED TO CONNECT!</b></li>";
-						dieout("</ul>");
-					}
-					print "OK</li>\n";
+				// 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 ";
 				}
 
-				## Tsearch2 checks
+				@$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\".";
@@ -255,176 +296,159 @@ class DatabasePostgres extends Database {
 					$this->doQuery($SQL);
 				}
 				print "OK</li>\n";
+			}
 
-
-				## Setup the schema for this user if needed
-				$result = $this->schemaExists($wgDBmwschema);
-				$safeschema = $this->quote_ident($wgDBmwschema);
+			// 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 "<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";
+					print "<b>FAILED</b>.</li>\n";
+					dieout("</ul>");
 				}
-				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";
+				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>. Could not set rights for the user.</li>\n";
+						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>");
 					}
-					$this->doQuery("SET search_path = $safeschema");
-					$rows = $this->numRows($res);
-					while ($rows) {
-						$rows--;
-						$this->doQuery(pg_fetch_result($res, $rows, 0));
-					}
-					print "OK</li>";
 				}
-
-				$wgDBsuperuser = '';
-				return true; ## Reconnect as regular user
-
-			} ## end superuser
-
-		if (!defined('POSTGRES_SEARCHPATH')) {
-
-			## 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');
-			foreach ( $ts2tables AS $tname ) {
-				$SQL = "SELECT count(*) FROM $wgDBts2schema.pg_ts_$tname";
+				$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> to access pg_ts_$tname. Make sure that the user ".
-					"\"$wgDBuser\" has SELECT access to all four tsearch2 tables</li>\n";
+					print "<b>FAILED</b>. Could not determine the tsearch2 locale information</li>\n";
 					dieout("</ul>");
 				}
-			}
-			$SQL = "SELECT ts_name FROM $wgDBts2schema.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>";
+				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\"...";
+				// 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;
 				}
-			}
-			if ($resetlocale) {
-				$SQL = "UPDATE $wgDBts2schema.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>");
+				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;
+					}
 				}
-				print "OK</li>";
-			}
-
-			## Final test: try out a simple tsearch2 query
-			$SQL = "SELECT $wgDBts2schema.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>";
-
-			## Do we have plpgsql installed?
-			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>";
+				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>";
 				}
-				else {
-					print "<b>FAILED</b>. You need to install the language plpgsql in the database <tt>$wgDBname</tt></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>";
 			}
-			print "OK</li>\n";
+			
+			// Install plpgsql if needed
+			$this->setup_plpgsql();
 
-			## Does the schema already exist? Who owns it?
+			// Does the schema already exist? Who owns it?
 			$result = $this->schemaExists($wgDBmwschema);
 			if (!$result) {
 				print "<li>Creating schema <b>$wgDBmwschema</b> ...";
 				error_reporting( 0 );
-				$result = $this->doQuery("CREATE SCHEMA $wgDBmwschema");
+				$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";
+						"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 != $user) {
-				print "<li>Schema \"$wgDBmwschema\" exists but is not owned by \"$user\". Not ideal.</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 \"$user\". Excellent.</li>\n";
+				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 \"$user\" ...";
+			
+			// 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) {
@@ -432,7 +456,7 @@ class DatabasePostgres extends Database {
 				dieout("</ul>");
 			}
 			print "OK</li>\n";
-			## Set for the rest of this session
+			// Set for the rest of this session
 			$SQL = "SET timezone = 'GMT'";
 			$result = pg_query($this->mConn, $SQL);
 			if (!$result) {
@@ -440,7 +464,7 @@ class DatabasePostgres extends Database {
 				dieout("</ul>");
 			}
 
-			print "<li>Setting the datestyle to ISO, YMD for user \"$user\" ...";
+			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) {
@@ -448,16 +472,16 @@ class DatabasePostgres extends Database {
 				dieout("</ul>");
 			}
 			print "OK</li>\n";
-			## Set for the rest of this session
+			// 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 \"$user\" ...";
+			
+			// 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);
@@ -470,7 +494,7 @@ class DatabasePostgres extends Database {
 				dieout("</ul>");
 			}
 			print "OK</li>\n";
-			## Set for the rest of this session
+			// Set for the rest of this session
 			$SQL = "SET search_path = $path";
 			$result = pg_query($this->mConn, $SQL);
 			if (!$result) {
@@ -478,17 +502,39 @@ class DatabasePostgres extends Database {
 				dieout("</ul>");
 			}
 			define( "POSTGRES_SEARCHPATH", $path );
-		}}
-
-		global $wgCommandLineMode;
-		## If called from the command-line (e.g. importDump), only show errors
-		if ($wgCommandLineMode) {
-			$this->doQuery("SET client_min_messages = 'ERROR'");
 		}
+	}
 
-		return $this->mConn;
+
+	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
@@ -503,6 +549,9 @@ class DatabasePostgres extends Database {
 	}
 
 	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);
 	}
 
@@ -759,6 +808,18 @@ class DatabasePostgres extends Database {
 		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
 	 */
@@ -897,9 +958,9 @@ class DatabasePostgres extends Database {
 
 
 	function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-		# Ignore errors during error handling to avoid infinite recursion
+		// Ignore errors during error handling to avoid infinite recursion
 		$ignore = $this->ignoreErrors( true );
-		++$this->mErrorCount;
+		$this->mErrorCount++;
 
 		if ($ignore || $tempIgnore) {
 			wfDebug("SQL ERROR (ignored): $error\n");
@@ -917,7 +978,7 @@ class DatabasePostgres extends Database {
 	/**
 	 * @return string wikitext of a link to the server software's web site
 	 */
-	function getSoftwareLink() {
+		function getSoftwareLink() {
 		return "[http://www.postgresql.org/ PostgreSQL]";
 	}
 
@@ -1074,13 +1135,14 @@ END;
 	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 = "mw_test_table";
+		// 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 $wgDBmwschema.$ctest");
+			$this->doQuery("DROP TABLE $safeschema.$ctest");
 		}
-		$SQL = "CREATE TABLE $wgDBmwschema.$ctest(a int)";
+		$SQL = "CREATE TABLE $safeschema.$ctest(a int)";
 		$olde = error_reporting( 0 );
 		$res = $this->doQuery($SQL);
 		error_reporting( $olde );
@@ -1088,19 +1150,9 @@ END;
 			print "<b>FAILED</b>. Make sure that the user \"$wgDBuser\" can write to the schema \"$wgDBmwschema\"</li>\n";
 			dieout("</ul>");
 		}
-		$this->doQuery("DROP TABLE $wgDBmwschema.mw_test_table");
+		$this->doQuery("DROP TABLE $safeschema.$ctest");
 
-		dbsource( "../maintenance/postgres/tables.sql", $this);
-
-		## Version-specific stuff
-		if ($this->numeric_version == 8.1) {
-			$this->doQuery("CREATE INDEX ts2_page_text ON pagecontent USING gist(textvector)");
-			$this->doQuery("CREATE INDEX ts2_page_title ON page USING gist(titlevector)");
-		}
-		else {
-			$this->doQuery("CREATE INDEX ts2_page_text ON pagecontent USING gin(textvector)");
-			$this->doQuery("CREATE INDEX ts2_page_title ON page USING gin(titlevector)");
-		}
+		$res = dbsource( "../maintenance/postgres/tables.sql", $this);
 
 		## Update version information
 		$mwv = $this->addQuotes($wgVersion);
@@ -1139,9 +1191,13 @@ END;
 	}
 
 	function encodeBlob( $b ) {
-		return pg_escape_bytea( $b );
+		return new Blob ( pg_escape_bytea( $b ) ) ;
 	}
+
 	function decodeBlob( $b ) {
+		if ($b instanceof Blob) {
+			$b = $b->fetch();
+		}
 		return pg_unescape_bytea( $b );
 	}
 
@@ -1152,11 +1208,10 @@ END;
 	function addQuotes( $s ) {
 		if ( is_null( $s ) ) {
 			return 'NULL';
-		} else if (is_array( $s )) { ## Assume it is bytea data
-			return "E'$s[1]'";
+		} else if ($s instanceof Blob) {
+			return "'".$s->fetch($s)."'";
 		}
 		return "'" . pg_escape_string($s) . "'";
-		// Unreachable: return "E'" . pg_escape_string($s) . "'";
 	}
 
 	function quote_ident( $s ) {
@@ -1168,6 +1223,32 @@ END;
 		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
 	 *
@@ -1223,6 +1304,10 @@ END;
 		return false;
 	}
 
+	function buildConcat( $stringList ) {
+		return implode( ' || ', $stringList );
+	}
+
 } // end DatabasePostgres class
 
 
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index ad682b72..376e55b1 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -13,7 +13,7 @@
  * depends on it.
  *
  * Documentation is in the source and on:
- * http://www.mediawiki.org/wiki/Help:Configuration_settings
+ * http://www.mediawiki.org/wiki/Manual:Configuration_settings
  *
  */
 
@@ -31,7 +31,7 @@ require_once( "$IP/includes/SiteConfiguration.php" );
 $wgConf = new SiteConfiguration;
 
 /** MediaWiki version number */
-$wgVersion			= '1.11.2';
+$wgVersion			= '1.12.0';
 
 /** Name of the site. It must be changed in LocalSettings.php */
 $wgSitename         = 'MediaWiki';
@@ -157,6 +157,7 @@ $wgUploadDirectory	= false; /// defaults to "{$IP}/images"
 $wgHashedUploadDirectory	= true;
 $wgLogo				= false; /// defaults to "{$wgStylePath}/common/images/wiki.png"
 $wgFavicon			= '/favicon.ico';
+$wgAppleTouchIcon   = false; /// This one'll actually default to off. For iPhone and iPod Touch web app bookmarks
 $wgMathPath         = false; /// defaults to "{$wgUploadPath}/math"
 $wgMathDirectory    = false; /// defaults to "{$wgUploadDirectory}/math"
 $wgTmpDirectory     = false; /// defaults to "{$wgUploadDirectory}/tmp"
@@ -458,7 +459,12 @@ $wgHashedSharedUploadDirectory = true;
  *
  * Please specify the namespace, as in the example below.
  */
-$wgRepositoryBaseUrl="http://commons.wikimedia.org/wiki/Image:";
+$wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/Image:";
+
+/**
+ * Experimental feature still under debugging.
+ */
+$wgFileRedirects = false;
 
 
 #
@@ -503,6 +509,16 @@ $wgEnableEmail = true;
  */
 $wgEnableUserEmail = true;
 
+/**
+ * Set to true to put the sending user's email in a Reply-To header
+ * instead of From. ($wgEmergencyContact will be used as From.)
+ *
+ * Some mailers (eg sSMTP) set the SMTP envelope sender to the From value,
+ * which can cause problems with SPF validation and leak recipient addressses
+ * when bounces are sent to the sender.
+ */
+$wgUserEmailUseReplyTo = false;
+
 /**
  * Minimum time, in hours, which must elapse between password reminder
  * emails for a given account. This is to prevent abuse by mail flooding.
@@ -594,7 +610,21 @@ $wgSharedDB = null;
 #   These and any other user-defined properties will be assigned to the mLBInfo member
 #   variable of the Database object.
 #
-# Leave at false to use the single-server variables above
+# Leave at false to use the single-server variables above. If you set this 
+# variable, the single-server variables will generally be ignored (except 
+# perhaps in some command-line scripts). 
+#
+# The first server listed in this array (with key 0) will be the master. The 
+# rest of the servers will be slaves. To prevent writes to your slaves due to 
+# accidental misconfiguration or MediaWiki bugs, set read_only=1 on all your 
+# slaves in my.cnf. You can set read_only mode at runtime using:
+#
+#     SET @@read_only=1;
+#
+# Since the effect of writing to a slave is so damaging and difficult to clean
+# up, we at Wikimedia set read_only=1 in my.cnf on all our DB servers, even 
+# our masters, and then set read_only=0 on masters at runtime. 
+#
 $wgDBservers		= false;
 
 /** How long to wait for a slave to catch up to the master */
@@ -606,6 +636,12 @@ $wgDBerrorLog		= false;
 /** When to give an error message */
 $wgDBClusterTimeout = 10;
 
+/**
+ * Scale load balancer polling time so that under overload conditions, the database server
+ * receives a SHOW STATUS query at an average interval of this many microseconds
+ */
+$wgDBAvgStatusPoll = 2000;
+
 /**
  * wgDBminWordLen :
  * MySQL 3.x : used to discard words that MySQL will not return any results for
@@ -643,7 +679,7 @@ $wgDBmysql5			= false;
  * account.
  * Array numeric key => database name
  */
-$wgLocalDatabases   = array();
+$wgLocalDatabases = array();
 
 /**
  * For multi-wiki clusters with multiple master servers; if an alternate
@@ -700,7 +736,7 @@ $wgCachedMessageArrays = false;
 # Language settings
 #
 /** Site language code, should be one of ./languages/Language(.*).php */
-$wgLanguageCode     = 'en';
+$wgLanguageCode = 'en';
 
 /**
  * Some languages need different word forms, usually for different cases.
@@ -715,6 +751,8 @@ $wgInterwikiMagic = true;
 /** Hide interlanguage links from the sidebar */
 $wgHideInterlanguageLinks = false;
 
+/** List of language names or overrides for default names in Names.php */
+$wgExtraLanguageNames = array();
 
 /** We speak UTF-8 all the time now, unless some oddities happen */
 $wgInputEncoding  = 'UTF-8';
@@ -792,6 +830,12 @@ $wgMsgCacheExpiry	= 86400;
  */
 $wgMaxMsgCacheEntrySize = 10000;
 
+/**
+ * Set to false if you are thorough system admin who always remembers to keep
+ * serialized files up to date to save few mtime calls.
+ */
+$wgCheckSerialized = true;
+
 # Whether to enable language variant conversion.
 $wgDisableLangConversion = false;
 
@@ -864,9 +908,19 @@ $wgRedirectSources = false;
 
 $wgShowIPinHeader	= true; # For non-logged in users
 $wgMaxNameChars		= 255;  # Maximum number of bytes in username
-$wgMaxSigChars      = 255;  # Maximum number of Unicode characters in signature
+$wgMaxSigChars		= 255;  # Maximum number of Unicode characters in signature
 $wgMaxArticleSize	= 2048; # Maximum article size in kilobytes
 
+$wgMaxPPNodeCount = 1000000;  # A complexity limit on template expansion
+
+/**
+ * Maximum recursion depth for templates within templates.
+ * The current parser adds two levels to the PHP call stack for each template, 
+ * and xdebug limits the call stack to 100 by default. So this should hopefully
+ * stop the parser before it hits the xdebug limit.
+ */
+$wgMaxTemplateDepth = 40;
+
 $wgExtraSubtitle	= '';
 $wgSiteSupportPage	= ''; # A page where you users can receive donations
 
@@ -958,6 +1012,11 @@ $wgEnableParserCache = true;
  */
 $wgEnableSidebarCache = false;
 
+/**
+ * Expiry time for the sidebar cache, in seconds
+ */
+$wgSidebarCacheExpiry = 86400;
+
 /**
  * Under which condition should a page in the main namespace be counted
  * as a valid article? If $wgUseCommaCount is set to true, it will be
@@ -1055,13 +1114,18 @@ $wgGroupPermissions['bot'  ]['bot']             = true;
 $wgGroupPermissions['bot'  ]['autoconfirmed']   = true;
 $wgGroupPermissions['bot'  ]['nominornewtalk']  = true;
 $wgGroupPermissions['bot'  ]['autopatrol']      = true;
+$wgGroupPermissions['bot'  ]['suppressredirect'] = true;
+$wgGroupPermissions['bot'  ]['apihighlimits']   = true;
 
 // Most extra permission abilities go to this group
 $wgGroupPermissions['sysop']['block']           = true;
 $wgGroupPermissions['sysop']['createaccount']   = true;
 $wgGroupPermissions['sysop']['delete']          = true;
+$wgGroupPermissions['sysop']['bigdelete']       = true; // can be separately configured for pages with > $wgDeleteRevisionsLimit revs
 $wgGroupPermissions['sysop']['deletedhistory'] 	= true; // can view deleted history entries, but not see or restore the text
+$wgGroupPermissions['sysop']['undelete']	= true;
 $wgGroupPermissions['sysop']['editinterface']   = true;
+$wgGroupPermissions['sysop']['editusercssjs']   = true;
 $wgGroupPermissions['sysop']['import']          = true;
 $wgGroupPermissions['sysop']['importupload']    = true;
 $wgGroupPermissions['sysop']['move']            = true;
@@ -1079,9 +1143,15 @@ $wgGroupPermissions['sysop']['autoconfirmed']   = true;
 $wgGroupPermissions['sysop']['upload_by_url']   = true;
 $wgGroupPermissions['sysop']['ipblock-exempt']	= true;
 $wgGroupPermissions['sysop']['blockemail']      = true;
+$wgGroupPermissions['sysop']['markbotedits']	= true;
+$wgGroupPermissions['sysop']['suppressredirect'] = true;
+$wgGroupPermissions['sysop']['apihighlimits']   = true;
+#$wgGroupPermissions['sysop']['mergehistory']    = true;
 
 // Permission to change users' group assignments
 $wgGroupPermissions['bureaucrat']['userrights'] = true;
+// Permission to change users' groups assignments across wikis
+#$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true;
 
 // Experimental permissions, not ready for production use
 //$wgGroupPermissions['sysop']['deleterevision'] = true;
@@ -1095,6 +1165,19 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true;
  */
 # $wgGroupPermissions['developer']['siteadmin'] = true;
 
+
+/**
+ * Implicit groups, aren't shown on Special:Listusers or somewhere else
+ */
+$wgImplicitGroups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' );
+
+/**
+ * These are the groups that users are allowed to add to or remove from
+ * their own account via Special:Userrights.
+ */
+$wgGroupsAddToSelf = array();
+$wgGroupsRemoveFromSelf = array();
+
 /**
  * Set of available actions that can be restricted via action=protect
  * You probably shouldn't change this.
@@ -1150,6 +1233,28 @@ $wgAutoConfirmAge = 0;
 $wgAutoConfirmCount = 0;
 //$wgAutoConfirmCount = 50;
 
+/**
+ * Automatically add a usergroup to any user who matches certain conditions.
+ * The format is
+ *   array( '&' or '|' or '^', cond1, cond2, ... )
+ * where cond1, cond2, ... are themselves conditions; *OR*
+ *   APCOND_EMAILCONFIRMED, *OR*
+ *   array( APCOND_EMAILCONFIRMED ), *OR*
+ *   array( APCOND_EDITCOUNT, number of edits ), *OR*
+ *   array( APCOND_AGE, seconds since registration ), *OR*
+ *   similar constructs defined by extensions.
+ *
+ * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any
+ * user who has provided an e-mail address.
+ */
+$wgAutopromote = array(
+	'autoconfirmed' => array( '&',
+		array( APCOND_EDITCOUNT, &$wgAutoConfirmCount ),
+		array( APCOND_AGE, &$wgAutoConfirmAge ),
+	),
+	'emailconfirmed' => APCOND_EMAILCONFIRMED,
+);
+
 /**
  * These settings can be used to give finer control over who can assign which
  * groups at Special:Userrights.  Example configuration:
@@ -1165,6 +1270,12 @@ $wgAutoConfirmCount = 0;
  */
 $wgAddGroups = $wgRemoveGroups = array();
 
+/**
+ * Optional to restrict deletion of pages with higher revision counts
+ * to users with the 'bigdelete' permission. (Default given to sysops.)
+ */
+$wgDeleteRevisionsLimit = 0;
+
 # Proxy scanner settings
 #
 
@@ -1214,7 +1325,7 @@ $wgCacheEpoch = '20030516000000';
  * to ensure that client-side caches don't keep obsolete copies of global
  * styles.
  */
-$wgStyleVersion = '97';
+$wgStyleVersion = '116';
 
 
 # Server-side caching:
@@ -1333,15 +1444,24 @@ $wgInternalServer = $wgServer;
 $wgSquidMaxage = 18000;
 
 /**
- * A list of proxy servers (ips if possible) to purge on changes don't specify
- * ports here (80 is default). When mediawiki is running behind a proxy, its
- * address should be listed in $wgSquidServers otherwise mediawiki won't rely
- * on the X-FORWARDED-FOR header to determine the user IP address and
- * all users will appear to come from the proxy IP address. Don't use domain
- * names here, only IP adresses.
+ * Default maximum age for raw CSS/JS accesses
+ */
+$wgForcedRawSMaxage = 300;
+
+/**
+ * List of proxy servers to purge on changes; default port is 80. Use IP addresses.
+ *
+ * When MediaWiki is running behind a proxy, it will trust X-Forwarded-For
+ * headers sent/modified from these proxies when obtaining the remote IP address
+ *
+ * For a list of trusted servers which *aren't* purged, see $wgSquidServersNoPurge.
  */
-# $wgSquidServers = array('127.0.0.1');
 $wgSquidServers = array();
+
+/**
+ * As above, except these servers aren't purged on page changes; use to set a
+ * list of trusted proxies, etc.
+ */
 $wgSquidServersNoPurge = array();
 
 /** Maximum number of titles to purge in any one client operation */
@@ -1442,6 +1562,14 @@ $wgDebugFunctionEntry = 0;
 /** Lots of debugging output from SquidUpdate.php */
 $wgDebugSquid = false;
 
+/*
+ * Destination for wfIncrStats() data...
+ * 'cache' to go into the system cache, if enabled (memcached)
+ * 'udp' to be sent to the UDP profiler (see $wgUDPProfilerHost)
+ * false to disable
+ */
+$wgStatsMethod = 'cache';
+
 /** Whereas to count the number of time an article is viewed.
  * Does not work if pages are cached (for example with squid).
  */
@@ -1598,9 +1726,11 @@ $wgMediaHandlers = array(
 	'image/png' => 'BitmapHandler',
 	'image/gif' => 'BitmapHandler',
 	'image/x-ms-bmp' => 'BmpHandler',
-	'image/svg+xml' => 'SvgHandler',
-	'image/svg' => 'SvgHandler',
-	'image/vnd.djvu' => 'DjVuHandler',
+	'image/svg+xml' => 'SvgHandler', // official
+	'image/svg' => 'SvgHandler', // compat
+	'image/vnd.djvu' => 'DjVuHandler', // official
+	'image/x.djvu' => 'DjVuHandler', // compat
+	'image/x-djvu' => 'DjVuHandler', // compat
 );
 
 
@@ -1688,7 +1818,7 @@ $wgIgnoreImageErrors = false;
 $wgGenerateThumbnailOnParse = true;
 
 /** Obsolete, always true, kept for compatibility with extensions */
-$wgUseImageResize		= true;
+$wgUseImageResize = true;
 
 
 /** Set $wgCommandLineMode if it's not set already, to avoid notices */
@@ -1848,7 +1978,19 @@ $wgAlwaysUseTidy = false;
 $wgTidyBin = 'tidy';
 $wgTidyConf = $IP.'/includes/tidy.conf';
 $wgTidyOpts = '';
-$wgTidyInternal = function_exists( 'tidy_load_config' );
+$wgTidyInternal = extension_loaded( 'tidy' );
+
+/**
+ * Put tidy warnings in HTML comments
+ * Only works for internal tidy.
+ */
+$wgDebugTidy = false;
+
+/**
+ * Validate the overall output using tidy and refuse 
+ * to display the page if it's not valid.
+ */
+$wgValidateAllHtml = false;
 
 /** See list of skins and their symbolic names in languages/Language.php */
 $wgDefaultSkin = 'monobook';
@@ -1920,7 +2062,11 @@ $wgSkinExtensionFunctions = array();
  * Extension messages files
  * Associative array mapping extension name to the filename where messages can be found.
  * The file must create a variable called $messages.
- * When the messages are needed, the extension should call wfLoadMessagesFile()
+ * When the messages are needed, the extension should call wfLoadExtensionMessages().
+ *
+ * Example: 
+ *    $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php';
+ *
  */
 $wgExtensionMessagesFiles = array();
 
@@ -1958,8 +2104,9 @@ $wgSpecialPages = array();
 $wgAutoloadClasses = array();
 
 /**
- * An array of extension types and inside that their names, versions, authors
- * and urls, note that the version and url key can be omitted.
+ * An array of extension types and inside that their names, versions, authors,
+ * urls, descriptions and pointers to localized description msgs. Note that
+ * the version, url, description and descriptionmsg key can be omitted.
  *
  * <code>
  * $wgExtensionCredits[$type][] = array(
@@ -1967,10 +2114,12 @@ $wgAutoloadClasses = array();
  *      'version' => 1.9,
  *	'author' => 'Foo Barstein',
  *	'url' => 'http://wwww.example.com/Example%20Extension/',
+ *	'description' => 'An example extension',
+ *	'descriptionmsg' => 'exampleextension-desc',
  * );
  * </code>
  *
- * Where $type is 'specialpage', 'parserhook', or 'other'.
+ * Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'.
  */
 $wgExtensionCredits = array();
 /*
@@ -2012,6 +2161,9 @@ $wgExternalDiffEngine = false;
 /** Use RC Patrolling to check for vandalism */
 $wgUseRCPatrol = true;
 
+/** Use new page patrolling to check new pages on special:Newpages */
+$wgUseNPPatrol = true;
+
 /** Set maximum number of results to return in syndication feeds (RSS, Atom) for
  * eg Recentchanges, Newpages. */
 $wgFeedLimit = 50;
@@ -2238,6 +2390,7 @@ $wgLogTypes = array( '',
 	'move',
 	'import',
 	'patrol',
+	'merge',
 );
 
 /**
@@ -2256,6 +2409,7 @@ $wgLogNames = array(
 	'move'    => 'movelogpage',
 	'import'  => 'importlogpage',
 	'patrol'  => 'patrol-log-page',
+	'merge'   => 'mergelog',
 );
 
 /**
@@ -2274,6 +2428,7 @@ $wgLogHeaders = array(
 	'move'    => 'movelogpagetext',
 	'import'  => 'importlogpagetext',
 	'patrol'  => 'patrol-log-header',
+	'merge'   => 'mergelogpagetext',
 );
 
 /**
@@ -2293,12 +2448,13 @@ $wgLogActions = array(
 	'delete/restore'    => 'undeletedarticle',
 	'delete/revision'   => 'revdelete-logentry',
 	'upload/upload'     => 'uploadedimage',
-	'upload/overwrite'	=> 'overwroteimage',
+	'upload/overwrite'  => 'overwroteimage',
 	'upload/revert'     => 'uploadedimage',
 	'move/move'         => '1movedto2',
 	'move/move_redir'   => '1movedto2_redir',
 	'import/upload'     => 'import-logentry-upload',
 	'import/interwiki'  => 'import-logentry-interwiki',
+	'merge/merge'       => 'pagemerge-logentry',
 );
 
 /**
@@ -2342,9 +2498,16 @@ $wgNoFollowLinks = true;
  */
 $wgNoFollowNsExceptions = array();
 
+/**
+ * Default robot policy.
+ * The default policy is to encourage indexing and following of links.
+ * It may be overridden on a per-namespace and/or per-page basis.
+ */
+$wgDefaultRobotPolicy = 'index,follow';
+
 /**
  * Robot policies per namespaces.
- * The default policy is 'index,follow', the array is made of namespace
+ * The default policy is given above, the array is made of namespace
  * constants as defined in includes/Defines.php
  * Example:
  *   $wgNamespaceRobotPolicies = array( NS_TALK => 'noindex' );
@@ -2423,7 +2586,7 @@ $wgRateLimits = array(
 	'edit' => array(
 		'anon'   => null, // for any and all anonymous edits (aggregate)
 		'user'   => null, // for each logged-in user
-		'newbie' => null, // for each recent account; overrides 'user'
+		'newbie' => null, // for each recent (autoconfirmed) account; overrides 'user'
 		'ip'     => null, // for each anon and recent account
 		'subnet' => null, // ... with final octet removed
 		),
@@ -2748,3 +2911,29 @@ $wgDisableOutputCompression = false;
  */
 $wgSlaveLagWarning = 10;
 $wgSlaveLagCritical = 30;
+
+/**
+ * Parser configuration. Associative array with the following members:
+ *
+ *     class        The class name
+ * 
+ * The entire associative array will be passed through to the constructor as 
+ * the first parameter. Note that only Setup.php can use this variable -- 
+ * the configuration will change at runtime via $wgParser member functions, so 
+ * the contents of this variable will be out-of-date. The variable can only be 
+ * changed during LocalSettings.php, in particular, it can't be changed during 
+ * an extension setup function. 
+ */
+$wgParserConf = array( 
+	'class' => 'Parser',
+);
+
+/**
+ * Hooks that are used for outputting exceptions
+ * Format is:
+ * 	$wgExceptionHooks[] = $funcname
+ * or:
+ * 	$wgExceptionHooks[] = array( $class, $funcname )
+ * Hooks should return strings or false
+ */
+$wgExceptionHooks = array();
diff --git a/includes/Defines.php b/includes/Defines.php
index c923c256..2d6aee5f 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -260,6 +260,27 @@ define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ );
 define( 'UTF8_HEAD', false );
 define( 'UTF8_TAIL', true );
 
-
-
-
+# Hook support constants
+define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 );
+define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 );
+
+# Allowed values for Parser::$mOutputType
+# Parameter to Parser::startExternalParse().
+define( 'OT_HTML', 1 );
+define( 'OT_WIKI', 2 );
+define( 'OT_PREPROCESS', 3 );
+define( 'OT_MSG' , 3 );  // b/c alias for OT_PREPROCESS
+
+# Flags for Parser::setFunctionHook
+define( 'SFH_NO_HASH', 1 );
+define( 'SFH_OBJECT_ARGS', 2 );
+
+# Flags for Parser::replaceLinkHolders
+define( 'RLH_FOR_UPDATE', 1 );
+
+# Autopromote conditions (must be here and not in Autopromote.php, so that
+# they're loaded for DefaultSettings.php before AutoLoader.php)
+define( 'APCOND_EDITCOUNT', 1 );
+define( 'APCOND_AGE', 2 );
+define( 'APCOND_EMAILCONFIRMED', 3 );
+define( 'APCOND_INGROUPS', 4 );
diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php
index 99bb4798..9aa17bbb 100644
--- a/includes/DifferenceEngine.php
+++ b/includes/DifferenceEngine.php
@@ -38,8 +38,9 @@ class DifferenceEngine {
 	 * @param $old Integer: old ID we want to show and diff with.
 	 * @param $new String: either 'prev' or 'next'.
 	 * @param $rcid Integer: ??? FIXME (default 0)
+	 * @param $refreshCache boolean If set, refreshes the diff cache
 	 */
-	function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0 ) {
+	function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) {
 		$this->mTitle = $titleObj;
 		wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
 
@@ -68,6 +69,7 @@ class DifferenceEngine {
 			$this->mNewid = intval($new);
 		}
 		$this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
+		$this->mRefreshCache = $refreshCache;
 	}
 
 	function showDiffPage( $diffOnly = false ) {
@@ -107,9 +109,8 @@ CONTROL;
 		$wgOut->setArticleFlag( false );
 		if ( ! $this->loadRevisionData() ) {
 			$t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})";
-			$mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
 			$wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
-			$wgOut->addWikitext( $mtext );
+			$wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
 			wfProfileOut( $fname );
 			return;
 		}
@@ -164,14 +165,15 @@ CONTROL;
 				$rcid = $this->mRcidMarkPatrolled;
 			} else {
 				// Look for an unpatrolled change corresponding to this diff
+				$db = wfGetDB( DB_SLAVE );
 				$change = RecentChange::newFromConds(
 					array(
-						// Add redundant timestamp condition so we can use the
-						// existing index
-						'rc_timestamp' => $this->mNewRev->getTimestamp(),
+						// Add redundant user,timestamp condition so we can use the existing index
+						'rc_user_text'  => $this->mNewRev->getRawUserText(),
+						'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
 						'rc_this_oldid' => $this->mNewid,
 						'rc_last_oldid' => $this->mOldid,
-						'rc_patrolled' => 0,
+						'rc_patrolled' => 0
 					),
 					__METHOD__
 				);
@@ -217,14 +219,49 @@ CONTROL;
 			$newminor = wfElement( 'span', array( 'class' => 'minor' ),
 			wfMsg( 'minoreditletter') ) . ' ';
 		}
+		
+		$rdel = ''; $ldel = '';
+		if( $wgUser->isAllowed( 'deleterevision' ) ) {
+			$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+			if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
+			// If revision was hidden from sysops
+				$ldel = wfMsgHtml('rev-delundel');	
+			} else {
+				$ldel = $sk->makeKnownLinkObj( $revdel,
+					wfMsgHtml('rev-delundel'),
+					'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
+					'&oldid=' . urlencode( $this->mOldRev->getId() ) );
+				// Bolden oversighted content
+				if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+					$ldel = "<strong>$ldel</strong>";
+			}
+			$ldel = "   <tt>(<small>$ldel</small>)</tt> ";
+			// We don't currently handle well changing the top revision's settings
+			if( $this->mNewRev->isCurrent() ) {
+			// If revision was hidden from sysops
+				$rdel = wfMsgHtml('rev-delundel');	
+			} else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
+			// If revision was hidden from sysops
+				$rdel = wfMsgHtml('rev-delundel');	
+			} else {
+				$rdel = $sk->makeKnownLinkObj( $revdel,
+					wfMsgHtml('rev-delundel'),
+					'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
+					'&oldid=' . urlencode( $this->mNewRev->getId() ) );
+				// Bolden oversighted content
+				if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+					$rdel = "<strong>$rdel</strong>";
+			}
+			$rdel = "   <tt>(<small>$rdel</small>)</tt> ";
+		}
 
-		$oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' .
-			'<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev ) . "</div>" .
-			'<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "</div>" .
-			'<div id="mw-diff-otitle4">' . $prevlink . '</div>';
-		$newHeader = '<div id="mw-diff-ntitle1"><strong>' .$this->mNewtitle . '</strong></div>' .
-			'<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev ) . " $rollback</div>" .
-			'<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "</div>" .
+		$oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
+			'<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
+			'<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" .
+			'<div id="mw-diff-otitle4">' . $prevlink .'</div>';
+		$newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
+			'<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" .
+			'<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
 			'<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
 
 		$this->showDiff( $oldHeader, $newHeader );
@@ -245,8 +282,10 @@ CONTROL;
 
 		$wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
 		#add deleted rev tag if needed
-		if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-		  	$wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+		if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+		  	$wgOut->addWikiMsg( 'rev-deleted-text-permission' );
+		} else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+		  	$wgOut->addWikiMsg( 'rev-deleted-text-view' );
 		}
 
 		if( !$this->mNewRev->isCurrent() ) {
@@ -258,7 +297,20 @@ CONTROL;
 			$wgOut->setRevisionId( $this->mNewRev->getId() );
 		}
 
-		$wgOut->addWikiTextTidy( $this->mNewtext );
+		if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) {
+			// Stolen from Article::view --AG 2007-10-11
+
+			// Give hooks a chance to customise the output
+			if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
+				// Wrap the whole lot in a <pre> and don't parse
+				$m = array();
+				preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+				$wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+				$wgOut->addHtml( htmlspecialchars( $this->mNewtext ) );
+				$wgOut->addHtml( "\n</pre>\n" );
+			}
+		} else
+			$wgOut->addWikiTextTidy( $this->mNewtext );
 
 		if( !$this->mNewRev->isCurrent() ) {
 			$wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
@@ -282,9 +334,8 @@ CONTROL;
 		if ( ! $this->loadNewText() ) {
 			$t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
 			  "{$this->mNewid})";
-			$mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
 			$wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
-			$wgOut->addWikitext( $mtext );
+			$wgOut->addWikiMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
 			wfProfileOut( $fname );
 			return;
 		}
@@ -324,10 +375,10 @@ CONTROL;
 	 * Returns false if the diff could not be generated, otherwise returns true
 	 */
 	function showDiff( $otitle, $ntitle ) {
-		global $wgOut, $wgRequest;
-		$diff = $this->getDiff( $otitle, $ntitle, $wgRequest->getVal( 'action' ) == 'purge' );
+		global $wgOut;
+		$diff = $this->getDiff( $otitle, $ntitle );
 		if ( $diff === false ) {
-			$wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) );
+			$wgOut->addWikiMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" );
 			return false;
 		} else {
 			$this->showDiffStyle();
@@ -352,11 +403,10 @@ CONTROL;
 	 *
 	 * @param Title $otitle Old title
 	 * @param Title $ntitle New title
-	 * @param bool $skipCache Skip the diff cache for this request?
 	 * @return mixed
 	 */
-	function getDiff( $otitle, $ntitle, $skipCache = false ) {
-		$body = $this->getDiffBody( $skipCache );
+	function getDiff( $otitle, $ntitle ) {
+		$body = $this->getDiffBody();
 		if ( $body === false ) {
 			return false;
 		} else {
@@ -368,43 +418,49 @@ CONTROL;
 	/**
 	 * Get the diff table body, without header
 	 *
-	 * @param bool $skipCache Skip cache for this request?
 	 * @return mixed
 	 */
-	function getDiffBody( $skipCache = false ) {
+	function getDiffBody() {
 		global $wgMemc;
 		$fname = 'DifferenceEngine::getDiffBody';
 		wfProfileIn( $fname );
 		
 		// Cacheable?
 		$key = false;
-		if ( $this->mOldid && $this->mNewid && !$skipCache ) {
-			// Try cache
+		if ( $this->mOldid && $this->mNewid ) {
 			$key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
-			$difftext = $wgMemc->get( $key );
-			if ( $difftext ) {
-				wfIncrStats( 'diff_cache_hit' );
-				$difftext = $this->localiseLineNumbers( $difftext );
-				$difftext .= "\n<!-- diff cache key $key -->\n";
-				wfProfileOut( $fname );
-				return $difftext;
-			}
+			// Try cache
+			if ( !$this->mRefreshCache ) {
+				$difftext = $wgMemc->get( $key );
+				if ( $difftext ) {
+					wfIncrStats( 'diff_cache_hit' );
+					$difftext = $this->localiseLineNumbers( $difftext );
+					$difftext .= "\n<!-- diff cache key $key -->\n";
+					wfProfileOut( $fname );
+					return $difftext;
+				}
+			} // don't try to load but save the result
 		}
 
-		#loadtext is permission safe, this just clears out the diff
+		// Loadtext is permission safe, this just clears out the diff
 		if ( !$this->loadText() ) {
 			wfProfileOut( $fname );
 			return false;
 		} else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
-		  return '';
+		  	return '';
 		} else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-		  return '';
+		  	return '';
 		}
 
 		$difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
 		
 		// Save to cache for 7 days
-		if ( $key !== false && $difftext !== false ) {
+		// Only do this for public revs, otherwise an admin can view the diff and a non-admin can nab it!
+		if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
+			wfIncrStats( 'diff_uncacheable' );
+		} else if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+			wfIncrStats( 'diff_uncacheable' );
+		} else if ( $key !== false && $difftext !== false ) {
 			wfIncrStats( 'diff_cache_miss' );
 			$wgMemc->set( $key, $difftext, 7*86400 );
 		} else {
@@ -536,15 +592,9 @@ CONTROL;
 	/**
 	 * Add the header to a diff body
 	 */
-	function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
+	static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
 		global $wgOut;
-	
-		if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
-		   $otitle = '<span class="history-deleted">'.$otitle.'</span>';
-		}
-		if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-		   $ntitle = '<span class="history-deleted">'.$ntitle.'</span>';
-		}
+
 		$header = "
 			<table class='diff'>
 			<col class='diff-marker' />
@@ -615,11 +665,16 @@ CONTROL;
 		} else {
 			$newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
 			$newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
-			$this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) );
+			$this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
 
 			$this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
 				. " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
 		}
+		if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+		  	$this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
+		} else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+		  	$this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
+		}
 
 		// Load the old revision object
 		$this->mOldRev = false;
@@ -647,12 +702,20 @@ CONTROL;
 			$t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
 			$oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
 			$oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
-			$this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) )
-				. "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
+			$this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
 			
+			$this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
+				. " (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
 			// Add an "undo" link
 			$newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
-			$this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
+			if ( $this->mNewRev->userCan(Revision::DELETED_TEXT) )
+				$this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
+			
+			if ( !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
+		  		$this->mOldtitle = "<span class='history-deleted'>{$this->mOldPagetitle}</span>";
+			} else if ( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
+		  		$this->mOldtitle = '<span class="history-deleted">'.$this->mOldtitle.'</span>';
+			}
 		}
 
 		return true;
@@ -673,7 +736,6 @@ CONTROL;
 			return false;
 		}
 		if ( $this->mOldRev ) {
-			// FIXME: permission tests
 			$this->mOldtext = $this->mOldRev->revText();
 			if ( $this->mOldtext === false ) {
 				return false;
@@ -1584,7 +1646,7 @@ class DiffFormatter
 	}
 
 	function _start_block($header) {
-		echo $header;
+		echo $header . "\n";
 	}
 
 	function _end_block() {
@@ -1613,6 +1675,84 @@ class DiffFormatter
 	}
 }
 
+/**
+ * A formatter that outputs unified diffs
+ * @addtogroup DifferenceEngine
+ */
+
+class UnifiedDiffFormatter extends DiffFormatter
+{
+	var $leading_context_lines = 2;
+	var $trailing_context_lines = 2;
+	
+	function _added($lines) {
+		$this->_lines($lines, '+');
+	}
+	function _deleted($lines) {
+		$this->_lines($lines, '-');
+	}
+	function _changed($orig, $closing) {
+		$this->_deleted($orig);
+		$this->_added($closing);
+	}
+	function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+		return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
+	}
+}
+
+/**
+ * A pseudo-formatter that just passes along the Diff::$edits array
+ * @addtogroup DifferenceEngine
+ */
+class ArrayDiffFormatter extends DiffFormatter
+{
+	function format($diff)
+	{
+		$oldline = 1;
+		$newline = 1;
+		$retval = array();
+		foreach($diff->edits as $edit)
+			switch($edit->type)
+			{
+				case 'add':
+					foreach($edit->closing as $l)
+					{
+						$retval[] = array(
+							'action' => 'add',
+							'new'=> $l,
+							'newline' => $newline++
+						);
+					}
+					break;
+				case 'delete':
+					foreach($edit->orig as $l)
+					{
+						$retval[] = array(
+							'action' => 'delete',
+							'old' => $l,
+							'oldline' => $oldline++,
+						);
+					}
+					break;
+				case 'change':
+					foreach($edit->orig as $i => $l)
+					{
+						$retval[] = array(
+							'action' => 'change',
+							'old' => $l,
+							'new' => @$edit->closing[$i],
+							'oldline' => $oldline++,
+							'newline' => $newline++,
+						);
+					}
+					break;
+				case 'copy':
+					$oldline += count($edit->orig);
+					$newline += count($edit->orig);
+			}
+		return $retval;
+	}			
+}
 
 /**
  *	Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
@@ -1828,13 +1968,15 @@ class TableDiffFormatter extends DiffFormatter
 	function _added( $lines ) {
 		foreach ($lines as $line) {
 			echo '<tr>' . $this->emptyLine() .
-				$this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n";
+				$this->addedLine( '<ins class="diffchange">' .
+					htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
 		}
 	}
 
 	function _deleted($lines) {
 		foreach ($lines as $line) {
-			echo '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) .
+			echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
+				htmlspecialchars ( $line ) . '</del>' ) .
 			  $this->emptyLine() . "</tr>\n";
 		}
 	}
diff --git a/includes/EditPage.php b/includes/EditPage.php
index cceb053d..8c3a37d4 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -8,8 +8,39 @@
  * The actual database and text munging is still in Article,
  * but it should get easier to call those from alternate
  * interfaces.
+ *
+ * EditPage cares about two distinct titles:
+ * $wgTitle is the page that forms submit to, links point to,
+ * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
+ * page in the database that is actually being edited. These are
+ * usually the same, but they are now allowed to be different.
  */
 class EditPage {
+	const AS_SUCCESS_UPDATE			= 200;
+	const AS_SUCCESS_NEW_ARTICLE		= 201;
+	const AS_HOOK_ERROR			= 210;
+	const AS_FILTERING			= 211;
+	const AS_HOOK_ERROR_EXPECTED		= 212;
+	const AS_BLOCKED_PAGE_FOR_USER		= 215;
+	const AS_CONTENT_TOO_BIG		= 216;
+	const AS_USER_CANNOT_EDIT		= 217;
+	const AS_READ_ONLY_PAGE_ANON		= 218;
+	const AS_READ_ONLY_PAGE_LOGGED		= 219;
+	const AS_READ_ONLY_PAGE			= 220;
+	const AS_RATE_LIMITED			= 221;
+	const AS_ARTICLE_WAS_DELETED		= 222;
+	const AS_NO_CREATE_PERMISSION		= 223;
+	const AS_BLANK_ARTICLE			= 224;
+	const AS_CONFLICT_DETECTED		= 225;
+	const AS_SUMMARY_NEEDED			= 226;
+	const AS_TEXTBOX_EMPTY			= 228;
+	const AS_MAX_ARTICLE_SIZE_EXCEEDED	= 229;
+	const AS_OK				= 230;
+	const AS_END				= 231;
+	const AS_SPAM_ERROR			= 232;
+	const AS_IMAGE_REDIRECT_ANON		= 233;
+	const AS_IMAGE_REDIRECT_LOGGED		= 234;
+
 	var $mArticle;
 	var $mTitle;
 	var $mMetaData = '';
@@ -42,9 +73,15 @@ class EditPage {
 	# extensions should take care to _append_ to the present value
 	public $editFormPageTop; // Before even the preview
 	public $editFormTextTop;
+	public $editFormTextBeforeContent;
 	public $editFormTextAfterWarn;
 	public $editFormTextAfterTools;
 	public $editFormTextBottom;
+	
+	/* $didSave should be set to true whenever an article was succesfully altered. */
+	public $didSave = false;
+	
+	public $suppressIntro = false;
 
 	/**
 	 * @todo document
@@ -52,12 +89,12 @@ class EditPage {
 	 */
 	function EditPage( $article ) {
 		$this->mArticle =& $article;
-		global $wgTitle;
-		$this->mTitle =& $wgTitle;
+		$this->mTitle = $article->getTitle();
 
 		# Placeholders for text injection by hooks (empty per default)
 		$this->editFormPageTop =
 		$this->editFormTextTop =
+		$this->editFormTextBeforeContent =
 		$this->editFormTextAfterWarn =
 		$this->editFormTextAfterTools =
 		$this->editFormTextBottom = "";
@@ -65,8 +102,9 @@ class EditPage {
 	
 	/**
 	 * Fetch initial editing page content.
+	 * @private
 	 */
-	private function getContent( $def_text = '' ) {
+	function getContent( $def_text = '' ) {
 		global $wgOut, $wgRequest, $wgParser;
 
 		# Get variables from query string :P
@@ -297,14 +335,13 @@ class EditPage {
 	 * the newly-edited page.
 	 */
 	function edit() {
-		global $wgOut, $wgUser, $wgRequest, $wgTitle;
+		global $wgOut, $wgUser, $wgRequest;
 
-		if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) )
+		if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) )
 			return;
 
-		$fname = 'EditPage::edit';
-		wfProfileIn( $fname );
-		wfDebug( "$fname: enter\n" );
+		wfProfileIn( __METHOD__ );
+		wfDebug( __METHOD__.": enter\n" );
 
 		// this is not an article
 		$wgOut->setArticleFlag(false);
@@ -314,13 +351,28 @@ class EditPage {
 
 		if( $this->live ) {
 			$this->livePreview();
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
+			return;
+		}
+		
+		if( wfReadOnly() ) {
+			$wgOut->readOnlyPage( $this->getContent() );
+			wfProfileOut( __METHOD__ );
 			return;
 		}
 
-		$permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser);
-		if( !$this->mTitle->exists() )
-			$permErrors += $this->mTitle->getUserPermissionsErrors( 'create', $wgUser);
+		$permErrors = $this->mTitle->getUserPermissionsErrors('edit', $wgUser);
+		if( !$this->mTitle->exists() ) {
+			# We can't use array_diff here, because that considers ANY TWO
+			# ARRAYS TO BE EQUAL.  Thanks, PHP.
+			$createErrors = $this->mTitle->getUserPermissionsErrors('create', $wgUser);
+			foreach( $createErrors as $error ) {
+				# in_array() actually *does* work as expected.
+				if( !in_array( $error, $permErrors ) ) {
+					$permErrors[] = $error;
+				}
+			}
+		}
 
 		# Ignore some permissions errors.
 		$remove = array();
@@ -341,14 +393,12 @@ class EditPage {
 				}
 			}
 		}
-		# array_diff returns elements in $permErrors that are not in $remove.
 		$permErrors = array_diff( $permErrors, $remove );
 
-		if ( !empty($permErrors) )
-		{
-			wfDebug( "$fname: User can't edit\n" );
+		if ( !empty($permErrors) ) {
+			wfDebug( __METHOD__.": User can't edit\n" );
 			$wgOut->readOnlyPage( $this->getContent(), true, $permErrors );
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
 			return;
 		} else {
 			if ( $this->save ) {
@@ -368,12 +418,12 @@ class EditPage {
 			}
 		}
 
-		wfProfileIn( "$fname-business-end" );
+		wfProfileIn( __METHOD__."-business-end" );
 
 		$this->isConflict = false;
 		// css / js subpages of user pages get a special treatment
-		$this->isCssJsSubpage      = $wgTitle->isCssJsSubpage();
-		$this->isValidCssJsSubpage = $wgTitle->isValidCssJsSubpage();
+		$this->isCssJsSubpage      = $this->mTitle->isCssJsSubpage();
+		$this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage();
 
 		/* Notice that we can't use isDeleted, because it returns true if article is ever deleted
 		 * no matter it's current state
@@ -401,7 +451,7 @@ class EditPage {
 			$this->showIntro();
 	
 		if( $this->mTitle->isTalkPage() ) {
-			$wgOut->addWikiText( wfMsg( 'talkpagetext' ) );
+			$wgOut->addWikiMsg( 'talkpagetext' );
 		}
 
 		# Attempt submission here.  This will check for edit conflicts,
@@ -411,8 +461,8 @@ class EditPage {
 
 		if ( 'save' == $this->formtype ) {
 			if ( !$this->attemptSave() ) {
-				wfProfileOut( "$fname-business-end" );
-				wfProfileOut( $fname );
+				wfProfileOut( __METHOD__."-business-end" );
+				wfProfileOut( __METHOD__ );
 				return;
 			}
 		}
@@ -422,8 +472,8 @@ class EditPage {
 		if ( 'initial' == $this->formtype || $this->firsttime ) {
 			if ($this->initialiseForm() === false) {
 				$this->noSuchSectionPage();
-				wfProfileOut( "$fname-business-end" );
-				wfProfileOut( $fname );
+				wfProfileOut( __METHOD__."-business-end" );
+				wfProfileOut( __METHOD__ );
 				return;
 			}
 			if( !$this->mTitle->getArticleId() ) 
@@ -431,8 +481,8 @@ class EditPage {
 		}
 
 		$this->showEditForm();
-		wfProfileOut( "$fname-business-end" );
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__."-business-end" );
+		wfProfileOut( __METHOD__ );
 	}
 
 	/**
@@ -587,16 +637,32 @@ class EditPage {
 	 */
 	private function showIntro() {
 		global $wgOut, $wgUser;
+		if( $this->suppressIntro )
+			return;
+
+		# Show a warning message when someone creates/edits a user (talk) page but the user does not exists
+		if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
+			$parts = explode( '/', $this->mTitle->getText(), 2 );
+			$username = $parts[0];
+			$id = User::idFromName( $username );
+			$ip = User::isIP( $username );
+
+			if ( $id == 0 && !$ip ) {
+				$wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>', 
+					array( 'userpage-userdoesnotexist', $username ) );
+			}
+		}
+
 		if( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
 			if( $wgUser->isLoggedIn() ) {
-				$wgOut->addWikiText( wfMsg( 'newarticletext' ) );
+				$wgOut->wrapWikiMsg( '<div class="mw-newarticletext">$1</div>', 'newarticletext' );
 			} else {
-				$wgOut->addWikiText( wfMsg( 'newarticletextanon' ) );
+				$wgOut->wrapWikiMsg( '<div class="mw-newarticletextanon">$1</div>', 'newarticletextanon' );
 			}
 			$this->showDeletionLog( $wgOut );
 		}
 	}
-	
+
 	/**
 	 * Attempt to show a custom editing introduction, if supplied
 	 *
@@ -619,11 +685,11 @@ class EditPage {
 	}
 
 	/**
-	 * Attempt submission
-	 * @return bool false if output is done, true if the rest of the form should be displayed
+	 * Attempt submission (no UI)
+	 * @return one of the constants describing the result
 	 */
-	function attemptSave() {
-		global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut;
+	function internalAttemptSave( &$result, $bot = false ) {
+		global $wgSpamRegex, $wgFilterCallback, $wgUser, $wgOut, $wgParser;
 		global $wgMaxArticleSize;
 
 		$fname = 'EditPage::attemptSave';
@@ -633,7 +699,18 @@ class EditPage {
 		if( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) )
 		{
 			wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
-			return false;
+			return self::AS_HOOK_ERROR;
+		}
+
+		# Check image redirect
+		if ( $this->mTitle->getNamespace() == NS_IMAGE &&
+			Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
+			!$wgUser->isAllowed( 'upload' ) ) {
+				if( $wgUser->isAnon() ) {
+					return self::AS_IMAGE_REDIRECT_ANON;
+				} else {
+					return self::AS_IMAGE_REDIRECT_LOGGED;
+				}
 		}
 
 		# Reintegrate metadata
@@ -643,34 +720,33 @@ class EditPage {
 		# Check for spam
 		$matches = array();
 		if ( $wgSpamRegex && preg_match( $wgSpamRegex, $this->textbox1, $matches ) ) {
-			$this->spamPage ( $matches[0] );
+			$result['spam'] = $matches[0];
 			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return false;
+			return self::AS_SPAM_ERROR;
 		}
 		if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section ) ) {
 			# Error messages or other handling should be performed by the filter function
-			wfProfileOut( $fname );
 			wfProfileOut( "$fname-checks" );
-			return false;
+			wfProfileOut( $fname );
+			return self::AS_FILTERING;
 		}
 		if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError ) ) ) {
 			# Error messages etc. could be handled within the hook...
-			wfProfileOut( $fname );
 			wfProfileOut( "$fname-checks" );
-			return false;
+			wfProfileOut( $fname );
+			return self::AS_HOOK_ERROR;
 		} elseif( $this->hookError != '' ) {
 			# ...or the hook could be expecting us to produce an error
-			wfProfileOut( "$fname-checks " );
+			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return true;
+			return self::AS_HOOK_ERROR_EXPECTED;
 		}
 		if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
 			# Check block state against master, thus 'false'.
-			$this->blockedPage();
 			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return false;
+			return self::AS_BLOCKED_PAGE_FOR_USER;
 		}
 		$this->kblength = (int)(strlen( $this->textbox1 ) / 1024);
 		if ( $this->kblength > $wgMaxArticleSize ) {
@@ -678,35 +754,31 @@ class EditPage {
 			$this->tooBig = true;
 			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return true;
+			return self::AS_CONTENT_TOO_BIG;
 		}
 
 		if ( !$wgUser->isAllowed('edit') ) {
 			if ( $wgUser->isAnon() ) {
-				$this->userNotLoggedInPage();
 				wfProfileOut( "$fname-checks" );
 				wfProfileOut( $fname );
-				return false;
+				return self::AS_READ_ONLY_PAGE_ANON;
 			}
 			else {
-				$wgOut->readOnlyPage();
 				wfProfileOut( "$fname-checks" );
 				wfProfileOut( $fname );
-				return false;
+				return self::AS_READ_ONLY_PAGE_LOGGED;
 			}
 		}
 
 		if ( wfReadOnly() ) {
-			$wgOut->readOnlyPage();
 			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return false;
+			return self::AS_READ_ONLY_PAGE;
 		}
 		if ( $wgUser->pingLimiter() ) {
-			$wgOut->rateLimited();
 			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return false;
+			return self::AS_RATE_LIMITED;
 		}
 
 		# If the article has been deleted while editing, don't save it without
@@ -714,7 +786,7 @@ class EditPage {
 		if ( $this->deletedSinceEdit && !$this->recreate ) {
 			wfProfileOut( "$fname-checks" );
 			wfProfileOut( $fname );
-			return true;
+			return self::AS_ARTICLE_WAS_DELETED;
 		}
 
 		wfProfileOut( "$fname-checks" );
@@ -726,24 +798,30 @@ class EditPage {
 			// Late check for create permission, just in case *PARANOIA*
 			if ( !$this->mTitle->userCan( 'create' ) ) {
 				wfDebug( "$fname: no create permission\n" );
-				$this->noCreatePermission();
 				wfProfileOut( $fname );
-				return;
+				return self::AS_NO_CREATE_PERMISSION;
 			}
 
 			# Don't save a new article if it's blank.
-			if ( ( '' == $this->textbox1 ) ) {
-					$wgOut->redirect( $this->mTitle->getFullURL() );
+			if ( '' == $this->textbox1 ) {
 					wfProfileOut( $fname );
-					return false;
+					return self::AS_BLANK_ARTICLE;
 			}
 
-			$isComment=($this->section=='new');
+			// Run post-section-merge edit filter
+			if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError ) ) ) {
+				# Error messages etc. could be handled within the hook...
+				wfProfileOut( $fname );
+				return self::AS_HOOK_ERROR;
+			}
+
+			$isComment = ( $this->section == 'new' );
+			
 			$this->mArticle->insertNewArticle( $this->textbox1, $this->summary,
-				$this->minoredit, $this->watchthis, false, $isComment);
+				$this->minoredit, $this->watchthis, false, $isComment, $bot);
 
 			wfProfileOut( $fname );
-			return false;
+			return self::AS_SUCCESS_NEW_ARTICLE;
 		}
 
 		# Article exists. Check for edit conflict.
@@ -808,18 +886,25 @@ class EditPage {
 
 		if ( $this->isConflict ) {
 			wfProfileOut( $fname );
-			return true;
+			return self::AS_CONFLICT_DETECTED;
 		}
 
 		$oldtext = $this->mArticle->getContent();
 
+		// Run post-section-merge edit filter
+		if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError ) ) ) {
+			# Error messages etc. could be handled within the hook...
+			wfProfileOut( $fname );
+			return self::AS_HOOK_ERROR;
+		}
+
 		# Handle the user preference to force summaries here, but not for null edits
 		if( $this->section != 'new' && !$this->allowBlankSummary && $wgUser->getOption( 'forceeditsummary')
 			&&  0 != strcmp($oldtext, $text) && !Article::getRedirectAutosummary( $text )) {
 			if( md5( $this->summary ) == $this->autoSumm ) {
 				$this->missingSummary = true;
 				wfProfileOut( $fname );
-				return( true );
+				return self::AS_SUMMARY_NEEDED;
 			}
 		}
 
@@ -828,7 +913,7 @@ class EditPage {
 			if (trim($this->summary) == '') {
 				$this->missingSummary = true;
 				wfProfileOut( $fname );
-				return( true );
+				return self::AS_SUMMARY_NEEDED;
 			}
 		}
 
@@ -838,14 +923,14 @@ class EditPage {
 		if( $this->section == 'new' ) {
 			if ( $this->textbox1 == '' ) {
 				$this->missingComment = true;
-				return true;
+				return self::AS_TEXTBOX_EMPTY;
 			}
 			if( $this->summary != '' ) {
-				$sectionanchor = $this->sectionAnchor( $this->summary );
+				$sectionanchor = $wgParser->guessSectionNameFromWikiText( $this->summary );
 				# This is a new section, so create a link to the new section
 				# in the revision summary.
-				$this->summary = wfMsgForContent('newsectionsummary') . 
-					" [[{$this->mTitle->getPrefixedText()}#{$this->summary}|{$this->summary}]]";
+				$cleanSummary = $wgParser->stripSectionName( $this->summary );
+				$this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 			}
 		} elseif( $this->section != '' ) {
 			# Try to get a section anchor from the section source, redirect to edited section if header found
@@ -855,7 +940,7 @@ class EditPage {
 			# we can't deal with anchors, includes, html etc in the header for now,
 			# headline would need to be parsed to improve this
 			if($hasmatch and strlen($matches[2]) > 0) {
-				$sectionanchor = $this->sectionAnchor( $matches[2] );
+				$sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] );
 			}
 		}
 		wfProfileOut( "$fname-sectionanchor" );
@@ -872,19 +957,19 @@ class EditPage {
 		if ( $this->kblength > $wgMaxArticleSize ) {
 			$this->tooBig = true;
 			wfProfileOut( $fname );
-			return true;
+			return self::AS_MAX_ARTICLE_SIZE_EXCEEDED;
 		}
 
 		# update the article here
 		if( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit,
-			$this->watchthis, '', $sectionanchor ) ) {
+			$this->watchthis, $bot, $sectionanchor ) ) {
 			wfProfileOut( $fname );
-			return false;
+			return self::AS_SUCCESS_UPDATE;
 		} else {
 			$this->isConflict = true;
 		}
 		wfProfileOut( $fname );
-		return true;
+		return self::AS_END;
 	}
 
 	/**
@@ -897,8 +982,8 @@ class EditPage {
 		$this->textbox1 = $this->getContent(false);
 		if ($this->textbox1 === false) return false;
 
-		if ( !$this->mArticle->exists() && $this->mArticle->mTitle->getNamespace() == NS_MEDIAWIKI )
-			$this->textbox1 = wfMsgWeirdKey( $this->mArticle->mTitle->getText() );
+		if ( !$this->mArticle->exists() && $this->mTitle->getNamespace() == NS_MEDIAWIKI )
+			$this->textbox1 = wfMsgWeirdKey( $this->mTitle->getText() );
 		wfProxyCheck();
 		return true;
 	}
@@ -910,7 +995,7 @@ class EditPage {
 	 *                      near the top, for captchas and the like.
 	 */
 	function showEditForm( $formCallback=null ) {
-		global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize;
+		global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle;
 
 		$fname = 'EditPage::showEditForm';
 		wfProfileIn( $fname );
@@ -929,116 +1014,123 @@ class EditPage {
 		}
 
 		if ( $this->isConflict ) {
-			$s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() );
+			$s = wfMsg( 'editconflict', $wgTitle->getPrefixedText() );
 			$wgOut->setPageTitle( $s );
-			$wgOut->addWikiText( wfMsg( 'explainconflict' ) );
+			$wgOut->addWikiMsg( 'explainconflict' );
 
 			$this->textbox2 = $this->textbox1;
 			$this->textbox1 = $this->getContent();
 			$this->edittime = $this->mArticle->getTimestamp();
 		} else {
-
 			if( $this->section != '' ) {
 				if( $this->section == 'new' ) {
-					$s = wfMsg('editingcomment', $this->mTitle->getPrefixedText() );
+					$s = wfMsg('editingcomment', $wgTitle->getPrefixedText() );
 				} else {
-					$s = wfMsg('editingsection', $this->mTitle->getPrefixedText() );
+					$s = wfMsg('editingsection', $wgTitle->getPrefixedText() );
 					$matches = array();
 					if( !$this->summary && !$this->preview && !$this->diff ) {
 						preg_match( "/^(=+)(.+)\\1/mi",
 							$this->textbox1,
 							$matches );
 						if( !empty( $matches[2] ) ) {
-							$this->summary = "/* ". trim($matches[2])." */ ";
+							global $wgParser;
+							$this->summary = "/* " . 
+								$wgParser->stripSectionName(trim($matches[2])) . 
+								" */ ";
 						}
 					}
 				}
 			} else {
-				$s = wfMsg( 'editing', $this->mTitle->getPrefixedText() );
+				$s = wfMsg( 'editing', $wgTitle->getPrefixedText() );
 			}
 			$wgOut->setPageTitle( $s );
 
 			if ( $this->missingComment ) {
-				$wgOut->addWikiText( wfMsg( 'missingcommenttext' ) );
+				$wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>',  'missingcommenttext' );
 			}
 
 			if( $this->missingSummary && $this->section != 'new' ) {
-				$wgOut->addWikiText( wfMsg( 'missingsummary' ) );
+				$wgOut->wrapWikiMsg( '<div id="mw-missingsummary">$1</div>', 'missingsummary' );
 			}
 
 			if( $this->missingSummary && $this->section == 'new' ) {
-				$wgOut->addWikiText( wfMsg( 'missingcommentheader' ) );
+				$wgOut->wrapWikiMsg( '<div id="mw-missingcommentheader">$1</div>', 'missingcommentheader' );
 			}
 
-			if( !$this->hookError == '' ) {
+			if( $this->hookError !== '' ) {
 				$wgOut->addWikiText( $this->hookError );
 			}
 
 			if ( !$this->checkUnicodeCompliantBrowser() ) {
-				$wgOut->addWikiText( wfMsg( 'nonunicodebrowser') );
+				$wgOut->addWikiMsg( 'nonunicodebrowser' );
 			}
 			if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) {
 			// Let sysop know that this will make private content public if saved
-				if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
-					$wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+				
+				if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) {
+					$wgOut->addWikiMsg( 'rev-deleted-text-permission' );
+				} else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
+					$wgOut->addWikiMsg( 'rev-deleted-text-view' );
 				}
+				
 				if( !$this->mArticle->mRevision->isCurrent() ) {
 					$this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() );
-					$wgOut->addWikiText( wfMsg( 'editingold' ) );
+					$wgOut->addWikiMsg( 'editingold' );
 				}
 			}
 		}
 
 		if( wfReadOnly() ) {
-			$wgOut->addWikiText( wfMsg( 'readonlywarning' ) );
+			$wgOut->addHTML( '<div id="mw-read-only-warning">'.wfMsgWikiHTML( 'readonlywarning' ).'</div>' );
 		} elseif( $wgUser->isAnon() && $this->formtype != 'preview' ) {
-			$wgOut->addWikiText( wfMsg( 'anoneditwarning' ) );
+			$wgOut->addHTML( '<div id="mw-anon-edit-warning">'.wfMsgWikiHTML( 'anoneditwarning' ).'</div>' );
 		} else {
 			if( $this->isCssJsSubpage && $this->formtype != 'preview' ) {
 				# Check the skin exists
 				if( $this->isValidCssJsSubpage ) {
-					$wgOut->addWikiText( wfMsg( 'usercssjsyoucanpreview' ) );
+					$wgOut->addWikiMsg( 'usercssjsyoucanpreview' );
 				} else {
-					$wgOut->addWikiText( wfMsg( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) );
+					$wgOut->addWikiMsg( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() );
 				}
 			}
 		}
 
 		if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 			# Show a warning if editing an interface message
-			$wgOut->addWikiText( wfMsg( 'editinginterface' ) );
+			$wgOut->addWikiMsg( 'editinginterface' );
 		} elseif( $this->mTitle->isProtected( 'edit' ) ) {
 			# Is the title semi-protected?
 			if( $this->mTitle->isSemiProtected() ) {
-				$notice = wfMsg( 'semiprotectedpagewarning' );
-				if( wfEmptyMsg( 'semiprotectedpagewarning', $notice ) || $notice == '-' )
-					$notice = '';
+				$noticeMsg = 'semiprotectedpagewarning';
 			} else {
-			# Then it must be protected based on static groups (regular)
-				$notice = wfMsg( 'protectedpagewarning' );
+				# Then it must be protected based on static groups (regular)
+				$noticeMsg = 'protectedpagewarning';
 			}
-			$wgOut->addWikiText( $notice );
+			$wgOut->addWikiMsg( $noticeMsg );
 		}
 		if ( $this->mTitle->isCascadeProtected() ) {
 			# Is this page under cascading protection from some source pages?
 			list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources();
+			$notice = "$1\n";
 			if ( count($cascadeSources) > 0 ) {
 				# Explain, and list the titles responsible
-				$notice = wfMsgExt( 'cascadeprotectedwarning', array('parsemag'), count($cascadeSources) ) . "\n";
 				foreach( $cascadeSources as $page ) {
 					$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
 				}
 			}
-			$wgOut->addWikiText( $notice );
+			$wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', count($cascadeSources) ) );
+		}
+		if( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) != array() ){
+			$wgOut->addWikiMsg( 'titleprotectedwarning' );
 		}
 
 		if ( $this->kblength === false ) {
 			$this->kblength = (int)(strlen( $this->textbox1 ) / 1024);
 		}
 		if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
-			$wgOut->addWikiText( wfMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize ) );
+			$wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgMaxArticleSize );
 		} elseif( $this->kblength > 29 ) {
-			$wgOut->addWikiText( wfMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) ) );
+			$wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) );
 		}
 
 		#need to parse the preview early so that we know which templates are used,
@@ -1056,12 +1148,12 @@ class EditPage {
 
 		$q = 'action=submit';
 		#if ( "no" == $redirect ) { $q .= "&redirect=no"; }
-		$action = $this->mTitle->escapeLocalURL( $q );
+		$action = $wgTitle->escapeLocalURL( $q );
 
 		$summary = wfMsg('summary');
 		$subject = wfMsg('subject');
 
-		$cancel = $sk->makeKnownLink( $this->mTitle->getPrefixedText(),
+		$cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(),
 				wfMsgExt('cancel', array('parseinline')) );
 		$edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ));
 		$edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'.
@@ -1069,10 +1161,14 @@ class EditPage {
 			htmlspecialchars( wfMsg( 'newwindow' ) );
 
 		global $wgRightsText;
-		$copywarn = "<div id=\"editpage-copywarn\">\n" .
-			wfMsg( $wgRightsText ? 'copyrightwarning' : 'copyrightwarning2',
+		if ( $wgRightsText ) {
+			$copywarnMsg = array( 'copyrightwarning', 
 				'[[' . wfMsgForContent( 'copyrightpage' ) . ']]',
-				$wgRightsText ) . "\n</div>";
+				$wgRightsText );
+		} else {
+			$copywarnMsg = array( 'copyrightwarning2', 
+				'[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
+		}
 
 		if( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) {
 			# prepare toolbar for edit buttons
@@ -1151,7 +1247,7 @@ class EditPage {
 		$recreate = '';
 		if ($this->deletedSinceEdit) {
 			if ( 'save' != $this->formtype ) {
-				$wgOut->addWikiText( wfMsg('deletedwhileediting'));
+				$wgOut->addWikiMsg('deletedwhileediting');
 			} else {
 				// Hide the toolbar and edit area, use can click preview to get it back
 				// Add an confirmation checkbox and explanation.
@@ -1200,6 +1296,7 @@ END
 $recreate
 {$commentsubject}
 {$subjectpreview}
+{$this->editFormTextBeforeContent}
 <textarea tabindex='1' accesskey="," name="wpTextbox1" id="wpTextbox1" rows='{$rows}'
 cols='{$cols}'{$ew} $hidden>
 END
@@ -1208,7 +1305,7 @@ END
 </textarea>
 		" );
 
-		$wgOut->addWikiText( $copywarn );
+		$wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg );
 		$wgOut->addHTML( $this->editFormTextAfterWarn );
 		$wgOut->addHTML( "
 {$metadata}
@@ -1226,7 +1323,7 @@ END
 </div><!-- editOptions -->");
 
 		$wgOut->addHtml( '<div class="mw-editTools">' );
-		$wgOut->addWikiText( wfMsgForContent( 'edittools' ) );
+		$wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
 		$wgOut->addHtml( '</div>' );
 
 		$wgOut->addHTML( $this->editFormTextAfterTools );
@@ -1267,14 +1364,14 @@ END
 		$wgOut->addHtml( wfHidden( 'wpAutoSummary', $autosumm ) );
 
 		if ( $this->isConflict ) {
-			$wgOut->addWikiText( '==' . wfMsg( "yourdiff" ) . '==' );
+			$wgOut->wrapWikiMsg( '==$1==', "yourdiff" );
 
 			$de = new DifferenceEngine( $this->mTitle );
 			$de->setText( $this->textbox2, $this->textbox1 );
 			$de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) );
 
-			$wgOut->addWikiText( '==' . wfMsg( "yourtext" ) . '==' );
-			$wgOut->addHTML( "<textarea tabindex=6 id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}' wrap='virtual'>"
+			$wgOut->wrapWikiMsg( '==$1==', "yourtext" );
+			$wgOut->addHTML( "<textarea tabindex='6' id='wpTextbox2' name=\"wpTextbox2\" rows='{$rows}' cols='{$cols}'>"
 				. htmlspecialchars( $this->safeUnicodeOutput( $this->textbox2 ) ) . "\n</textarea>" );
 		}
 		$wgOut->addHTML( $this->editFormTextBottom );
@@ -1333,8 +1430,7 @@ END
 			htmlspecialchars( "$wgStylePath/common/preview.js?$wgStyleVersion" ) .
 			'"></script>' . "\n" );
 		$liveAction = $wgTitle->getLocalUrl( 'action=submit&wpPreview=true&live=true' );
-		return "return !livePreview(" .
-			"getElementById('wikiPreview')," .
+		return "return !lpDoPreview(" .
 			"editform.wpTextbox1.value," .
 			'"' . $liveAction . '"' . ")";
 	}
@@ -1382,17 +1478,12 @@ END
 
 		if ( $this->mTriedSave && !$this->mTokenOk ) {
 			if ( $this->mTokenOkExceptSuffix ) {
-				$msg = 'token_suffix_mismatch';
+				$note = wfMsg( 'token_suffix_mismatch' );
 			} else {
-				$msg = 'session_fail_preview';
+				$note = wfMsg( 'session_fail_preview' );
 			}
 		} else {
-			$msg = 'previewnote';
-		}
-		$previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" .
-			"<div class='previewnote'>" . $wgOut->parse( wfMsg( $msg ) ) . "</div>\n";
-		if ( $this->isConflict ) {
-			$previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
+			$note = wfMsg( 'previewnote' );
 		}
 
 		$parserOptions = ParserOptions::newFromUser( $wgUser );
@@ -1410,16 +1501,15 @@ END
 		# XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here
 
 		if ( $this->isCssJsSubpage ) {
-			if(preg_match("/\\.css$/", $wgTitle->getText() ) ) {
+			if(preg_match("/\\.css$/", $this->mTitle->getText() ) ) {
 				$previewtext = wfMsg('usercsspreview');
-			} else if(preg_match("/\\.js$/", $wgTitle->getText() ) ) {
+			} else if(preg_match("/\\.js$/", $this->mTitle->getText() ) ) {
 				$previewtext = wfMsg('userjspreview');
 			}
 			$parserOptions->setTidy(true);
-			$parserOutput = $wgParser->parse( $previewtext , $wgTitle, $parserOptions );
+			$parserOutput = $wgParser->parse( $previewtext , $this->mTitle, $parserOptions );
 			$wgOut->addHTML( $parserOutput->mText );
-			wfProfileOut( $fname );
-			return $previewhead;
+			$previewHTML = '';
 		} else {
 			$toparse = $this->textbox1;
 
@@ -1431,22 +1521,38 @@ END
 
 			if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData ;
 			$parserOptions->setTidy(true);
+			$parserOptions->enableLimitReport();
 			$parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ) ."\n\n",
-					$wgTitle, $parserOptions );
+					$this->mTitle, $parserOptions );
 
 			$previewHTML = $parserOutput->getText();
 			$wgOut->addParserOutputNoText( $parserOutput );
 			
 			# ParserOutput might have altered the page title, so reset it
-			$wgOut->setPageTitle( wfMsg( 'editing', $this->mTitle->getPrefixedText() ) );			
+			# Also, use the title defined by DISPLAYTITLE magic word when present
+			if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) {
+				$wgOut->setPageTitle( wfMsg( 'editing', $dt ) );
+			} else {
+				$wgOut->setPageTitle( wfMsg( 'editing', $wgTitle->getPrefixedText() ) );			
+			}
 
 			foreach ( $parserOutput->getTemplates() as $ns => $template)
 				foreach ( array_keys( $template ) as $dbk)
 					$this->mPreviewTemplates[] = Title::makeTitle($ns, $dbk);
 
-			wfProfileOut( $fname );
-			return $previewhead . $previewHTML;
+			if ( count( $parserOutput->getWarnings() ) ) {
+				$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+			}
 		}
+
+		$previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" .
+			"<div class='previewnote'>" . $wgOut->parse( $note ) . "</div>\n";
+		if ( $this->isConflict ) {
+			$previewhead.='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
+		}
+
+		wfProfileOut( $fname );
+		return $previewhead . $previewHTML;
 	}
 
 	/**
@@ -1471,7 +1577,7 @@ END
 			$cols = $wgUser->getOption( 'cols' );
 			$attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' );
 			$wgOut->addHtml( '<hr />' );
-			$wgOut->addWikiText( wfMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() ) );
+			$wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() );
 			$wgOut->addHtml( wfOpenElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . wfCloseElement( 'textarea' ) );
 		}
 	}
@@ -1480,34 +1586,18 @@ END
 	 * Produce the stock "please login to edit pages" page
 	 */
 	function userNotLoggedInPage() {
-		global $wgUser, $wgOut;
+		global $wgUser, $wgOut, $wgTitle;
 		$skin = $wgUser->getSkin();
 
 		$loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
-		$loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $this->mTitle->getPrefixedUrl() );
+		$loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() );
 
 		$wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) );
 		$wgOut->setRobotPolicy( 'noindex,nofollow' );
 		$wgOut->setArticleRelated( false );
 
 		$wgOut->addHtml( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) );
-		$wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() );
-	}
-
-	/**
-	 * Creates a basic error page which informs the user that
-	 * they have to validate their email address before being
-	 * allowed to edit.
-	 */
-	function userNotConfirmedPage() {
-		global $wgOut;
-
-		$wgOut->setPageTitle( wfMsg( 'confirmedittitle' ) );
-		$wgOut->setRobotPolicy( 'noindex,nofollow' );
-		$wgOut->setArticleRelated( false );
-
-		$wgOut->addWikiText( wfMsg( 'confirmedittext' ) );
-		$wgOut->returnToMain( false );
+		$wgOut->returnToMain( false, $wgTitle );
 	}
 
 	/**
@@ -1515,14 +1605,14 @@ END
 	 * they have attempted to edit a nonexistant section.
 	 */
 	function noSuchSectionPage() {
-		global $wgOut;
+		global $wgOut, $wgTitle;
 
 		$wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) );
 		$wgOut->setRobotPolicy( 'noindex,nofollow' );
 		$wgOut->setArticleRelated( false );
 
-		$wgOut->addWikiText( wfMsg( 'nosuchsectiontext', $this->section ) );
-		$wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() );
+		$wgOut->addWikiMsg( 'nosuchsectiontext', $this->section );
+		$wgOut->returnToMain( false, $wgTitle );
 	}
 
 	/**
@@ -1531,17 +1621,19 @@ END
 	 * @param $match Text which triggered one or more filters
 	 */
 	function spamPage( $match = false ) {
-		global $wgOut;
+		global $wgOut, $wgTitle;
 
 		$wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) );
 		$wgOut->setRobotPolicy( 'noindex,nofollow' );
 		$wgOut->setArticleRelated( false );
 
-		$wgOut->addWikiText( wfMsg( 'spamprotectiontext' ) );
+		$wgOut->addHtml( '<div id="spamprotected">' );
+		$wgOut->addWikiMsg( 'spamprotectiontext' );
 		if ( $match )
-			$wgOut->addWikiText( wfMsg( 'spamprotectionmatch', "<nowiki>{$match}</nowiki>" ) );
+			$wgOut->addWikiMsg( 'spamprotectionmatch',wfEscapeWikiText( $match ) );
+		$wgOut->addHtml( '</div>' );
 
-		$wgOut->returnToMain( false );
+		$wgOut->returnToMain( false, $wgTitle );
 	}
 
 	/**
@@ -1556,7 +1648,7 @@ END
 
 		// This is the revision the editor started from
 		$baseRevision = Revision::loadFromTimestamp(
-			$db, $this->mArticle->mTitle, $this->edittime );
+			$db, $this->mTitle, $this->edittime );
 		if( is_null( $baseRevision ) ) {
 			wfProfileOut( $fname );
 			return false;
@@ -1565,7 +1657,7 @@ END
 
 		// The current state, we want to merge updates into it
 		$currentRevision =  Revision::loadFromTitle(
-			$db, $this->mArticle->mTitle );
+			$db, $this->mTitle );
 		if( is_null( $currentRevision ) ) {
 			wfProfileOut( $fname );
 			return false;
@@ -1605,6 +1697,14 @@ END
 		return true;
 	}
 
+	/**
+	 * @deprecated use $wgParser->stripSectionName()
+	 */
+	function pseudoParseSectionAnchor( $text ) {
+		global $wgParser;
+		return $wgParser->stripSectionName( $text );
+	}
+
 	/**
 	 * Format an anchor fragment as it would appear for a given section name
 	 * @param string $text
@@ -1612,19 +1712,8 @@ END
 	 * @private
 	 */
 	function sectionAnchor( $text ) {
-		$headline = Sanitizer::decodeCharReferences( $text );
-		# strip out HTML
-		$headline = preg_replace( '/<.*?' . '>/', '', $headline );
-		$headline = trim( $headline );
-		$sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
-		$replacearray = array(
-			'%3A' => ':',
-			'%' => '.'
-		);
-		return str_replace(
-			array_keys( $replacearray ),
-			array_values( $replacearray ),
-			$sectionanchor );
+		global $wgParser;
+		return $wgParser->guessSectionNameFromWikiText( $text );
 	}
 
 	/**
@@ -1649,16 +1738,16 @@ END
 		$toolarray = array(
 			array(	'image'	=> 'button_bold.png',
 				'id'	=> 'mw-editbutton-bold',
-				'open'	=> '\\\'\\\'\\\'',
-				'close'	=> '\\\'\\\'\\\'',
+				'open'	=> '\'\'\'',
+				'close'	=> '\'\'\'',
 				'sample'=> wfMsg('bold_sample'),
 				'tip'	=> wfMsg('bold_tip'),
 				'key'	=> 'B'
 			),
 			array(	'image'	=> 'button_italic.png',
 				'id'	=> 'mw-editbutton-italic',
-				'open'	=> '\\\'\\\'',
-				'close'	=> '\\\'\\\'',
+				'open'	=> '\'\'',
+				'close'	=> '\'\'',
 				'sample'=> wfMsg('italic_sample'),
 				'tip'	=> wfMsg('italic_tip'),
 				'key'	=> 'I'
@@ -1681,8 +1770,8 @@ END
 			),
 			array(	'image'	=> 'button_headline.png',
 				'id'	=> 'mw-editbutton-headline',
-				'open'	=> "\\n== ",
-				'close'	=> " ==\\n",
+				'open'	=> "\n== ",
+				'close'	=> " ==\n",
 				'sample'=> wfMsg('headline_sample'),
 				'tip'	=> wfMsg('headline_tip'),
 				'key'	=> 'H'
@@ -1706,7 +1795,7 @@ END
 			array(	'image'	=> 'button_math.png',
 				'id'	=> 'mw-editbutton-math',
 				'open'	=> "<math>",
-				'close'	=> "<\\/math>",
+				'close'	=> "</math>",
 				'sample'=> wfMsg('math_sample'),
 				'tip'	=> wfMsg('math_tip'),
 				'key'	=> 'C'
@@ -1714,7 +1803,7 @@ END
 			array(	'image'	=> 'button_nowiki.png',
 				'id'	=> 'mw-editbutton-nowiki',
 				'open'	=> "<nowiki>",
-				'close'	=> "<\\/nowiki>",
+				'close'	=> "</nowiki>",
 				'sample'=> wfMsg('nowiki_sample'),
 				'tip'	=> wfMsg('nowiki_tip'),
 				'key'	=> 'N'
@@ -1729,7 +1818,7 @@ END
 			),
 			array(	'image'	=> 'button_hr.png',
 				'id'	=> 'mw-editbutton-hr',
-				'open'	=> "\\n----\\n",
+				'open'	=> "\n----\n",
 				'close'	=> '',
 				'sample'=> '',
 				'tip'	=> wfMsg('hr_tip'),
@@ -1740,22 +1829,22 @@ END
 		$toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n";
 
 		foreach($toolarray as $tool) {
-
-			$cssId = $tool['id'];
-			$image=$wgStylePath.'/common/images/'.$tool['image'];
-			$open=$tool['open'];
-			$close=$tool['close'];
-			$sample = wfEscapeJsString( $tool['sample'] );
-
-			// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
-			// Older browsers show a "speedtip" type message only for ALT.
-			// Ideally these should be different, realistically they
-			// probably don't need to be.
-			$tip = wfEscapeJsString( $tool['tip'] );
-
-			#$key = $tool["key"];
-
-			$toolbar.="addButton('$image','$tip','$open','$close','$sample','$cssId');\n";
+			$params = array(
+				$image = $wgStylePath.'/common/images/'.$tool['image'],
+				// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
+				// Older browsers show a "speedtip" type message only for ALT.
+				// Ideally these should be different, realistically they
+				// probably don't need to be.
+				$tip = $tool['tip'],
+				$open = $tool['open'],
+				$close = $tool['close'],
+				$sample = $tool['sample'],
+				$cssId = $tool['id'],
+			);
+			
+			$paramList = implode( ',',
+				array_map( array( 'Xml', 'encodeJsVar' ), $params ) );
+			$toolbar.="addButton($paramList);\n";
 		}
 
 		$toolbar.="/*]]>*/\n</script>";
@@ -1880,7 +1969,8 @@ END
 			'title'     => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']',
 		);
 		$buttons['diff'] = wfElement('input', $temp, '');
-
+		
+		wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons ) );
 		return $buttons;
 	}
 
@@ -1902,12 +1992,15 @@ END
 		header( 'Content-type: text/xml; charset=utf-8' );
 		header( 'Cache-control: no-cache' );
 
+		$previewText = $this->getPreviewText();
+		#$categories = $skin->getCategoryLinks();
+
 		$s =
 		'<?xml version="1.0" encoding="UTF-8" ?>' . "\n" .
-		Xml::openElement( 'livepreview' ) .
-		Xml::element( 'preview', null, $this->getPreviewText() ) .
-		Xml::element( 'br', array( 'style' => 'clear: both;' ) ) .
-		Xml::closeElement( 'livepreview' );
+		Xml::tags( 'livepreview', null,
+			Xml::element( 'preview', null, $previewText )
+			#.	Xml::element( 'category', null, $categories )
+		);
 		echo $s;
 	}
 
@@ -2057,7 +2150,7 @@ END
 	function noCreatePermission() {
 		global $wgOut;
 		$wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) );
-		$wgOut->addWikiText( wfMsg( 'nocreatetext' ) );
+		$wgOut->addWikiMsg( 'nocreatetext' );
 	}
 	
 	/**
@@ -2067,7 +2160,7 @@ END
 	 * @param OutputPage $out
 	 */
 	private function showDeletionLog( $out ) {
-		$title = $this->mArticle->getTitle();
+		$title = $this->mTitle;
 		$reader = new LogReader(
 			new FauxRequest(
 				array(
@@ -2078,13 +2171,80 @@ END
 		);
 		if( $reader->hasRows() ) {
 			$out->addHtml( '<div id="mw-recreate-deleted-warn">' );
-			$out->addWikiText( wfMsg( 'recreate-deleted-warn' ) );
+			$out->addWikiMsg( 'recreate-deleted-warn' );
 			$viewer = new LogViewer( $reader );
 			$viewer->showList( $out );
-			$out->addHtml( '</div>' );			
-		}				
+			$out->addHtml( '</div>' );
+		}
 	}
-	
-}
 
+	/**
+	 * Attempt submission
+	 * @return bool false if output is done, true if the rest of the form should be displayed
+	 */
+	function attemptSave() {
+		global $wgUser, $wgOut, $wgTitle, $wgRequest;
+
+		$resultDetails = false;
+		$value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) );
+		
+		if( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) {
+			$this->didSave = true;
+		}
+
+		switch ($value) {
+			case self::AS_HOOK_ERROR_EXPECTED:
+			case self::AS_CONTENT_TOO_BIG:
+		 	case self::AS_ARTICLE_WAS_DELETED:
+			case self::AS_CONFLICT_DETECTED:
+			case self::AS_SUMMARY_NEEDED:
+			case self::AS_TEXTBOX_EMPTY:
+			case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
+			case self::AS_END:
+				return true;
+
+			case self::AS_HOOK_ERROR:
+			case self::AS_FILTERING:
+			case self::AS_SUCCESS_NEW_ARTICLE:
+			case self::AS_SUCCESS_UPDATE:
+				return false;
 
+			case self::AS_SPAM_ERROR:
+				$this->spamPage ( $resultDetails['spam'] );
+				return false;
+
+			case self::AS_BLOCKED_PAGE_FOR_USER:
+				$this->blockedPage();
+				return false;
+
+			case self::AS_IMAGE_REDIRECT_ANON:
+				$wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
+				return false;
+
+			case self::AS_READ_ONLY_PAGE_ANON:
+				$this->userNotLoggedInPage();
+				return false;
+
+		 	case self::AS_READ_ONLY_PAGE_LOGGED:
+		 	case self::AS_READ_ONLY_PAGE:
+		 		$wgOut->readOnlyPage();
+		 		return false;
+
+		 	case self::AS_RATE_LIMITED:
+		 		$wgOut->rateLimited();
+		 		return false;
+
+		 	case self::AS_NO_CREATE_PERMISSION;
+		 		$this->noCreatePermission();
+		 		return;
+		 	
+			case self::AS_BLANK_ARTICLE:
+		 		$wgOut->redirect( $wgTitle->getFullURL() );
+		 		return false;
+
+			case self::AS_IMAGE_REDIRECT_LOGGED:
+				$wgOut->permissionRequired( 'upload' );
+				return false;
+		}
+	}
+}
diff --git a/includes/Exception.php b/includes/Exception.php
index 02819cc9..2fd54352 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -16,6 +16,26 @@ class MWException extends Exception
 		return is_object( $wgLang );
 	}
 
+	function runHooks( $name, $args = array() ) {
+		global $wgExceptionHooks;
+		if( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) 
+			return;	// Just silently ignore
+		if( !array_key_exists( $name, $wgExceptionHooks ) || !is_array( $wgExceptionHooks[ $name ] ) )
+			return;
+		$hooks = $wgExceptionHooks[ $name ];
+		$callargs = array_merge( array( $this ), $args );
+
+		foreach( $hooks as $hook ) {
+			if( is_string( $hook ) || ( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) ) ) {	//'function' or array( 'class', hook' )
+				$result = call_user_func_array( $hook, $callargs );
+			} else {
+				$result = null;
+			}
+			if( is_string( $result ) )
+				return $result;
+		}
+	}
+
 	/** Get a message from i18n */
 	function msg( $key, $fallback /*[, params...] */ ) {
 		$args = array_slice( func_get_args(), 2 );
@@ -35,7 +55,8 @@ class MWException extends Exception
 				"</p>\n";
 		} else {
 			return "<p>Set <b><tt>\$wgShowExceptionDetails = true;</tt></b> " .
-				"in LocalSettings.php to show detailed debugging information.</p>";
+				"at the bottom of LocalSettings.php to show detailed " .
+				"debugging information.</p>";
 		}
 	}
 
@@ -82,27 +103,29 @@ class MWException extends Exception
 			$wgOut->enableClientCache( false );
 			$wgOut->redirect( '' );
 			$wgOut->clearHTML();
-			$wgOut->addHTML( $this->getHTML() );
+			if( $hookResult = $this->runHooks( get_class( $this ) ) ) {
+				$wgOut->addHTML( $hookResult );
+			} else {
+				$wgOut->addHTML( $this->getHTML() );
+			}
 			$wgOut->output();
 		} else {
+			if( $hookResult = $this->runHooks( get_class( $this ) . "Raw" ) ) {
+				die( $hookResult );
+			}
 			echo $this->htmlHeader();
 			echo $this->getHTML();
 			echo $this->htmlFooter();
 		}
 	}
 
-	/** Print the exception report using text */
-	function reportText() {
-		echo $this->getText();
-	}
-
 	/* Output a report about the exception and takes care of formatting.
 	 * It will be either HTML or plain text based on $wgCommandLineMode.
 	 */
 	function report() {
 		global $wgCommandLineMode;
 		if ( $wgCommandLineMode ) {
-			$this->reportText();
+			fwrite( STDERR, $this->getText() );
 		} else {
 			$log = $this->getLogMessage();
 			if ( $log ) {
@@ -135,7 +158,6 @@ class MWException extends Exception
 	function htmlFooter() {
 		echo "</body></html>";
 	}
-
 }
 
 /**
@@ -199,7 +221,7 @@ function wfReportException( Exception $e ) {
 			 $e2->__toString() . "\n";
 
 			 if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) {
-				 echo $message;
+				 fwrite( STDERR, $message );
 			 } else {
 				 echo nl2br( htmlspecialchars( $message ) ). "\n";
 			 }
diff --git a/includes/Export.php b/includes/Export.php
index c3ef9451..69d88fc6 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -111,7 +111,7 @@ class WikiExporter {
 	function pageByTitle( $title ) {
 		return $this->dumpFrom(
 			'page_namespace=' . $title->getNamespace() .
-			' AND page_title=' . $this->db->addQuotes( $title->getDbKey() ) );
+			' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) );
 	}
 
 	function pageByName( $name ) {
diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php
index f3fc22e3..f5ce5b9d 100644
--- a/includes/ExternalEdit.php
+++ b/includes/ExternalEdit.php
@@ -34,6 +34,7 @@ class ExternalEdit {
 		$name=$this->mTitle->getText();
 		$pos=strrpos($name,".")+1;
 		header ( "Content-type: application/x-external-editor; charset=".$this->mCharset );
+		header( "Cache-control: no-cache" );
 
 		# $type can be "Edit text", "Edit file" or "Diff text" at the moment
 		# See the protocol specifications at [[m:Help:External editors/Tech]] for
@@ -47,12 +48,7 @@ class ExternalEdit {
 		} elseif($this->mMode=="file") {
 			$type="Edit file";
 			$image = wfLocalFile( $this->mTitle );
-			$img_url = $image->getURL();
-			if(strpos($img_url,"://")) {
-				$url = $img_url;
-			} else {
-				$url = $wgServer . $img_url;
-			}
+			$url = $image->getFullURL();
 			$extension=substr($name, $pos);
 		}
 		$special=$wgLang->getNsText(NS_SPECIAL);
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
index 5efc6e25..79937b85 100644
--- a/includes/ExternalStore.php
+++ b/includes/ExternalStore.php
@@ -1,18 +1,15 @@
 <?php
 /**
- *
- *
  * Constructor class for data kept in external repositories
  *
  * External repositories might be populated by maintenance/async
  * scripts, thus partial moving of data may be possible, as well
  * as possibility to have any storage format (i.e. for archives)
- *
  */
 
 class ExternalStore {
 	/* Fetch data from given URL */
-	function fetchFromURL($url) {
+	static function fetchFromURL($url) {
 		global $wgExternalStores;
 
 		if (!$wgExternalStores)
@@ -32,7 +29,7 @@ class ExternalStore {
 	/**
 	 * Get an external store object of the given type
 	 */
-	function &getStoreObject( $proto ) {
+	static function &getStoreObject( $proto ) {
 		global $wgExternalStores;
 		if (!$wgExternalStores)
 			return false;
@@ -55,7 +52,7 @@ class ExternalStore {
 	 * class itself as a parameter.
 	 * Returns the URL of the stored data item, or false on error
 	 */
-	function insert( $url, $data ) {
+	static function insert( $url, $data ) {
 		list( $proto, $params ) = explode( '://', $url, 2 );
 		$store =& ExternalStore::getStoreObject( $proto );
 		if ( $store === false ) {
diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php
index cff6c4d4..ef907df5 100644
--- a/includes/ExternalStoreHttp.php
+++ b/includes/ExternalStoreHttp.php
@@ -9,9 +9,9 @@
 class ExternalStoreHttp {
 	/* Fetch data from given URL */
 	function fetchFromURL($url) {
-	ini_set( "allow_url_fopen", true );
-	$ret = file_get_contents( $url );
-	ini_set( "allow_url_fopen", false );
+		ini_set( "allow_url_fopen", true );
+		$ret = file_get_contents( $url );
+		ini_set( "allow_url_fopen", false );
 		return $ret;
 	}
 
diff --git a/includes/Feed.php b/includes/Feed.php
index ed4343c3..309b29bd 100644
--- a/includes/Feed.php
+++ b/includes/Feed.php
@@ -41,6 +41,7 @@ class FeedItem {
 
 	/**#@+
 	 * @todo document
+	 * @param $Url URL uniquely designating the item.
 	 */
 	function __construct( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) {
 		$this->Title = $Title;
@@ -145,12 +146,13 @@ class ChannelFeed extends FeedItem {
 	 * @private
 	 */
 	function outXmlHeader() {
-		global $wgServer, $wgStylePath, $wgStyleVersion;
+		global $wgStylePath, $wgStyleVersion;
 
 		$this->httpHeaders();
 		echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
 		echo '<?xml-stylesheet type="text/css" href="' .
-			htmlspecialchars( "$wgServer$wgStylePath/common/feed.css?$wgStyleVersion" ) . '"?' . ">\n";
+			htmlspecialchars( wfExpandUrl( "$wgStylePath/common/feed.css?$wgStyleVersion" ) ) .
+			'"?' . ">\n";
 	}
 }
 
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index ee165cd1..71e2c1ae 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -39,7 +39,7 @@ class FileDeleteForm {
 			$wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
 			return;
 		} elseif( !$wgUser->isAllowed( 'delete' ) ) {
-			$wgOut->permissionError( 'delete' );
+			$wgOut->permissionRequired( 'delete' );
 			return;
 		} elseif( $wgUser->isBlocked() ) {
 			$wgOut->blockedPage();
@@ -63,25 +63,37 @@ class FileDeleteForm {
 		
 		// Perform the deletion if appropriate
 		if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
-			$comment = $wgRequest->getText( 'wpReason' );
+			$this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList' );
+			$this->DeleteReason = $wgRequest->getText( 'wpReason' );
+			$reason = $this->DeleteReasonList;
+			if ( $reason != 'other' && $this->DeleteReason != '') {
+				// Entry from drop down menu + additional comment
+				$reason .= ': ' . $this->DeleteReason;
+			} elseif ( $reason == 'other' ) {
+				$reason = $this->DeleteReason;
+			}
 			if( $this->oldimage ) {
-				$status = $this->file->deleteOld( $this->oldimage, $comment );
+				$status = $this->file->deleteOld( $this->oldimage, $reason );
 				if( $status->ok ) {
 					// Need to do a log item
 					$log = new LogPage( 'delete' );
-					$log->addEntry( 'delete', $this->title, wfMsg( 'deletedrevision' , $this->oldimage ) );
+					$logComment = wfMsgForContent( 'deletedrevision', $this->oldimage );
+					if( trim( $reason ) != '' )
+						$logComment .= ": {$reason}";
+					$log->addEntry( 'delete', $this->title, $logComment );
 				}
 			} else {
-				$status = $this->file->delete( $comment );
+				$status = $this->file->delete( $reason );
 				if( $status->ok ) {
 					// Need to delete the associated article
 					$article = new Article( $this->title );
-					$article->doDeleteArticle( $comment );
+					$article->doDeleteArticle( $reason );
 				}
 			}
 			if( !$status->isGood() )
 				$wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) );
 			if( $status->ok ) {
+				$wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
 				$wgOut->addHtml( $this->prepareMessage( 'filedelete-success' ) );
 				// Return to the main page if we just deleted all versions of the
 				// file, otherwise go back to the description page
@@ -93,27 +105,51 @@ class FileDeleteForm {
 		$this->showForm();
 		$this->showLogEntries();
 	}
-	
+
 	/**
 	 * Show the confirmation form
 	 */
 	private function showForm() {
-		global $wgOut, $wgUser, $wgRequest;
-		
-		$form  = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) );
-		$form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) );
-		$form .= '<fieldset><legend>' . wfMsgHtml( 'filedelete-legend' ) . '</legend>';
-		$form .= $this->prepareMessage( 'filedelete-intro' );
-		
-		$form .= '<p>' . Xml::inputLabel( wfMsg( 'filedelete-comment' ), 'wpReason', 'wpReason',
-			60, $wgRequest->getText( 'wpReason' ) ) . '</p>';
-		$form .= '<p>' . Xml::submitButton( wfMsg( 'filedelete-submit' ) ) . '</p>';
-		$form .= '</fieldset>';
-		$form .= '</form>';
-		
+		global $wgOut, $wgUser, $wgRequest, $wgContLang;
+		$align = $wgContLang->isRtl() ? 'left' : 'right';
+
+		$form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) .
+			Xml::openElement( 'fieldset' ) .
+			Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) .
+			Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ) .
+			$this->prepareMessage( 'filedelete-intro' ) .
+			Xml::openElement( 'table' ) .
+			"<tr>
+				<td align='$align'>" .
+					Xml::label( wfMsg( 'filedelete-comment' ), 'wpDeleteReasonList' ) .
+				"</td>
+				<td>" .
+					Xml::listDropDown( 'wpDeleteReasonList',
+						wfMsgForContent( 'filedelete-reason-dropdown' ), 
+						wfMsgForContent( 'filedelete-reason-otherlist' ), '', 'wpReasonDropDown', 1 ) .
+				"</td>
+			</tr>
+			<tr>
+				<td align='$align'>" .
+					Xml::label( wfMsg( 'filedelete-otherreason' ), 'wpReason' ) .
+				"</td>
+				<td>" .
+					Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
+				"</td>
+			</tr>
+			<tr>
+				<td></td>
+				<td>" .
+					Xml::submitButton( wfMsg( 'filedelete-submit' ), array( 'name' => 'mw-filedelete-submit', 'id' => 'mw-filedelete-submit', 'tabindex' => '3' ) ) .
+				"</td>
+			</tr>" .
+			Xml::closeElement( 'table' ) .
+			Xml::closeElement( 'fieldset' ) .
+			Xml::closeElement( 'form' );
+
 		$wgOut->addHtml( $form );
 	}
-	
+
 	/**
 	 * Show deletion log fragments pertaining to the current file
 	 */
@@ -142,16 +178,16 @@ class FileDeleteForm {
 	 * @return string
 	 */
 	private function prepareMessage( $message ) {
-		global $wgLang, $wgServer;
+		global $wgLang;
 		if( $this->oldimage ) {
+			$url = $this->file->getArchiveUrl( $this->oldimage );
 			return wfMsgExt(
-				"{$message}-old",
+				"{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old'
 				'parse',
 				$this->title->getText(),
 				$wgLang->date( $this->getTimestamp(), true ),
 				$wgLang->time( $this->getTimestamp(), true ),
-				$wgServer . $this->file->getArchiveUrl( $this->oldimage )
-			);
+				wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) );
 		} else {
 			return wfMsgExt(
 				$message,
@@ -217,4 +253,4 @@ class FileDeleteForm {
 		return $this->oldfile->getTimestamp();
 	}
 	
-}
\ No newline at end of file
+}
diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php
index 55f21fff..f335d024 100644
--- a/includes/FileRevertForm.php
+++ b/includes/FileRevertForm.php
@@ -28,7 +28,7 @@ class FileRevertForm {
 	 * pending authentication, confirmation, etc.
 	 */
 	public function execute() {
-		global $wgOut, $wgRequest, $wgUser, $wgLang, $wgServer;
+		global $wgOut, $wgRequest, $wgUser, $wgLang;
 		$this->setHeaders();
 
 		if( wfReadOnly() ) {
@@ -71,7 +71,7 @@ class FileRevertForm {
 				$wgOut->addHtml( wfMsgExt( 'filerevert-success', 'parse', $this->title->getText(),
 					$wgLang->date( $this->getTimestamp(), true ),
 					$wgLang->time( $this->getTimestamp(), true ),
-					$wgServer . $this->file->getArchiveUrl( $this->oldimage ) ) );
+					wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ) );
 				$wgOut->returnToMain( false, $this->title );
 			} else {
 				$wgOut->addWikiText( $status->getWikiText() );
@@ -87,14 +87,15 @@ class FileRevertForm {
 	 * Show the confirmation form
 	 */
 	private function showForm() {
-		global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang, $wgServer;
+		global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang;
 		$timestamp = $this->getTimestamp();
 
 		$form  = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) );
 		$form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) );
 		$form .= '<fieldset><legend>' . wfMsgHtml( 'filerevert-legend' ) . '</legend>';
 		$form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(),
-			$wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), $wgServer . $this->file->getArchiveUrl( $this->oldimage ) );
+			$wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ),
+			wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) );
 		$form .= '<p>' . Xml::inputLabel( wfMsg( 'filerevert-comment' ), 'wpComment', 'wpComment',
 			60, wfMsgForContent( 'filerevert-defaultcomment',
 			$wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ) ) . '</p>';
diff --git a/includes/FileStore.php b/includes/FileStore.php
index 1554d66e..a547e7e4 100644
--- a/includes/FileStore.php
+++ b/includes/FileStore.php
@@ -162,7 +162,7 @@ class FileStore {
 	function delete( $key ) {
 		$destPath = $this->filePath( $key );
 		if( false === $destPath ) {
-			throw new FSExcepton( "file store does not contain file '$key'" );
+			throw new FSException( "file store does not contain file '$key'" );
 		} else {
 			return FileStore::deleteFile( $destPath );
 		}
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 67cc1f39..2b9543b4 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -8,20 +8,6 @@ if ( !defined( 'MEDIAWIKI' ) ) {
  * Global functions used everywhere
  */
 
-/**
- * Some globals and requires needed
- */
-
-/** Total number of articles */
-$wgNumberOfArticles = -1; # Unset
-
-/** Total number of views */
-$wgTotalViews = -1;
-
-/** Total number of edits */
-$wgTotalEdits = -1;
-
-
 require_once dirname(__FILE__) . '/LogPage.php';
 require_once dirname(__FILE__) . '/normal/UtfNormalUtil.php';
 require_once dirname(__FILE__) . '/XmlFunctions.php';
@@ -111,11 +97,6 @@ function wfClone( $object ) {
 	return clone( $object );
 }
 
-/**
- * Where as we got a random seed
- */
-$wgRandomSeeded = false;
-
 /**
  * Seed Mersenne Twister
  * No-op for compatibility; only necessary in PHP < 4.2.0
@@ -308,11 +289,6 @@ function wfReadOnly() {
  * Use wfMsgForContent() instead if the message should NOT
  * change depending on the user preferences.
  *
- * Note that the message may contain HTML, and is therefore
- * not safe for insertion anywhere. Some functions such as
- * addWikiText will do the escaping for you. Use wfMsgHtml()
- * if you need an escaped message.
- *
  * @param $key String: lookup key for the message, usually
  *    defined in languages/Language.php
  * 
@@ -416,11 +392,10 @@ function wfMsgNoDBForContent( $key ) {
  * @return String: the requested message.
  */
 function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) {
-	$fname = 'wfMsgReal';
-	wfProfileIn( $fname );
+	wfProfileIn( __METHOD__ );
 	$message = wfMsgGetKey( $key, $useDB, $forContent, $transform );
 	$message = wfMsgReplaceArgs( $message, $args );
-	wfProfileOut( $fname );
+	wfProfileOut( __METHOD__ );
 	return $message;
 }
 
@@ -447,24 +422,12 @@ function wfMsgWeirdKey ( $key ) {
 function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) {
 	global $wgParser, $wgContLang, $wgMessageCache, $wgLang;
 
-	/* <Vyznev> btw, is all that code in wfMsgGetKey() that check
-	 * if the message cache exists of not really necessary, or is
-	 * it just paranoia?
-	 * <TimStarling> Vyznev: it's probably not necessary
-	 * <TimStarling> I think I wrote it in an attempt to report DB
-	 * connection errors properly
-	 * <TimStarling> but eventually we gave up on using the
-	 * message cache for that and just hard-coded the strings
-	 * <TimStarling> it may have other uses, it's not mere paranoia
-	 */
-
-	if ( is_object( $wgMessageCache ) )
-		$transstat = $wgMessageCache->getTransform();
-
+	# If $wgMessageCache isn't initialised yet, try to return something sensible.
 	if( is_object( $wgMessageCache ) ) {
-		if ( ! $transform )
-			$wgMessageCache->disableTransform();
 		$message = $wgMessageCache->get( $key, $useDB, $forContent );
+		if ( $transform ) {
+			$message = $wgMessageCache->transform( $message );
+		}
 	} else {
 		if( $forContent ) {
 			$lang = &$wgContLang;
@@ -476,22 +439,13 @@ function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) {
 		# ISSUE: Should we try to handle "message/lang" here too?
 		$key = str_replace( ' ' , '_' , $wgContLang->lcfirst( $key ) );
 
-		wfSuppressWarnings();
 		if( is_object( $lang ) ) {
 			$message = $lang->getMessage( $key );
 		} else {
 			$message = false;
 		}
-		wfRestoreWarnings();
-
-		if ( $transform && strstr( $message, '{{' ) !== false ) {
-			$message = $wgParser->transformMsg($message, $wgMessageCache->getParserOptions() );
-		}
 	}
 
-	if ( is_object( $wgMessageCache ) && ! $transform )
-		$wgMessageCache->setTransform( $transstat );
-
 	return $message;
 }
 
@@ -511,15 +465,13 @@ function wfMsgReplaceArgs( $message, $args ) {
 	// Replace arguments
 	if ( count( $args ) ) {
 		if ( is_array( $args[0] ) ) {
-			foreach ( $args[0] as $key => $val ) {
-				$message = str_replace( '$' . $key, $val, $message );
-			}
-		} else {
-			foreach( $args as $n => $param ) {
-				$replacementKeys['$' . ($n + 1)] = $param;
-			}
-			$message = strtr( $message, $replacementKeys );
+			$args = array_values( $args[0] );
+		}
+		$replacementKeys = array();
+		foreach( $args as $n => $param ) {
+			$replacementKeys['$' . ($n + 1)] = $param;
 		}
+		$message = strtr( $message, $replacementKeys );
 	}
 
 	return $message;
@@ -566,9 +518,12 @@ function wfMsgWikiHtml( $key ) {
  * @param array $options Processing rules:
  *  <i>parse</i>: parses wikitext to html
  *  <i>parseinline</i>: parses wikitext to html and removes the surrounding p's added by parser or tidy
- *  <i>escape</i>: filters message trough htmlspecialchars
+ *  <i>escape</i>: filters message through htmlspecialchars
+ *  <i>escapenoentities</i>: same, but allows entity references like   through
  *  <i>replaceafter</i>: parameters are substituted after parsing or escaping
  *  <i>parsemag</i>: transform the message using magic phrases
+ *  <i>content</i>: fetch message for content language instead of interface
+ * Behavior for conflicting options (e.g., parse+parseinline) is undefined.
  */
 function wfMsgExt( $key, $options ) {
 	global $wgOut, $wgParser;
@@ -581,29 +536,38 @@ function wfMsgExt( $key, $options ) {
 		$options = array($options);
 	}
 
-	$string = wfMsgGetKey( $key, true, false, false );
+	$forContent = false;
+	if( in_array('content', $options) ) {
+		$forContent = true;
+	}
+
+	$string = wfMsgGetKey( $key, /*DB*/true, $forContent, /*Transform*/false );
 
 	if( !in_array('replaceafter', $options) ) {
 		$string = wfMsgReplaceArgs( $string, $args );
 	}
 
 	if( in_array('parse', $options) ) {
-		$string = $wgOut->parse( $string, true, true );
+		$string = $wgOut->parse( $string, true, !$forContent );
 	} elseif ( in_array('parseinline', $options) ) {
-		$string = $wgOut->parse( $string, true, true );
+		$string = $wgOut->parse( $string, true, !$forContent );
 		$m = array();
-		if( preg_match( '/^<p>(.*)\n?<\/p>$/sU', $string, $m ) ) {
+		if( preg_match( '/^<p>(.*)\n?<\/p>\n?$/sU', $string, $m ) ) {
 			$string = $m[1];
 		}
 	} elseif ( in_array('parsemag', $options) ) {
 		global $wgMessageCache;
 		if ( isset( $wgMessageCache ) ) {
-			$string = $wgMessageCache->transform( $string );
+			$string = $wgMessageCache->transform( $string, !$forContent );
 		}
 	}
 
 	if ( in_array('escape', $options) ) {
 		$string = htmlspecialchars ( $string );
+	} elseif ( in_array( 'escapenoentities', $options ) ) {
+		$string = htmlspecialchars( $string );
+		$string = str_replace( '&', '&', $string );
+		$string = Sanitizer::normalizeCharReferences( $string );
 	}
 
 	if( in_array('replaceafter', $options) ) {
@@ -903,8 +867,8 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
  */
 function wfEscapeWikiText( $text ) {
 	$text = str_replace(
-		array( '[',     '|',      '\'',    'ISBN ',     'RFC ',     '://',     "\n=",     '{{' ),
-		array( '[', '|', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ),
+		array( '[',     '|',      ']',     '\'',    'ISBN ',     'RFC ',     '://',     "\n=",     '{{' ),
+		array( '[', '|', ']', ''', 'ISBN ', 'RFC ', '://', "\n=", '{{' ),
 		htmlspecialchars($text) );
 	return $text;
 }
@@ -1009,6 +973,21 @@ function wfAppendQuery( $url, $query ) {
 	return $url;
 }
 
+/**
+ * Expand a potentially local URL to a fully-qualified URL.
+ * Assumes $wgServer is correct. :)
+ * @param string $url, either fully-qualified or a local path + query
+ * @return string Fully-qualified URL
+ */
+function wfExpandUrl( $url ) {
+	if( substr( $url, 0, 1 ) == '/' ) {
+		global $wgServer;
+		return $wgServer . $url;
+	} else {
+		return $url;
+	}
+}
+
 /**
  * This is obsolete, use SquidUpdate::purge()
  * @deprecated
@@ -1673,13 +1652,29 @@ function wfMkdirParents( $fullDir, $mode = 0777 ) {
 /**
  * Increment a statistics counter
  */
- function wfIncrStats( $key ) {
-	 global $wgMemc;
-	 $key = wfMemcKey( 'stats', $key );
-	 if ( is_null( $wgMemc->incr( $key ) ) ) {
-		 $wgMemc->add( $key, 1 );
-	 }
- }
+function wfIncrStats( $key ) {
+	global $wgStatsMethod;
+	
+	if( $wgStatsMethod == 'udp' ) {
+		global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname;
+		static $socket;
+		if (!$socket) {
+			$socket=socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
+			$statline="stats/{$wgDBname} - 1 1 1 1 1 -total\n";
+			socket_sendto($socket,$statline,strlen($statline),0,$wgUDPProfilerHost,$wgUDPProfilerPort);
+		}
+		$statline="stats/{$wgDBname} - 1 1 1 1 1 {$key}\n";
+		@socket_sendto($socket,$statline,strlen($statline),0,$wgUDPProfilerHost,$wgUDPProfilerPort);
+	} elseif( $wgStatsMethod == 'cache' ) {
+		global $wgMemc;
+		$key = wfMemcKey( 'stats', $key );
+		if ( is_null( $wgMemc->incr( $key ) ) ) {
+			$wgMemc->add( $key, 1 );
+		}
+	} else {
+		// Disabled
+	}
+}
 
 /**
  * @param mixed $nr The number to format
@@ -1772,6 +1767,38 @@ function wfUrlProtocols() {
 	}
 }
 
+/**
+ * Safety wrapper around ini_get() for boolean settings.
+ * The values returned from ini_get() are pre-normalized for settings
+ * set via php.ini or php_flag/php_admin_flag... but *not*
+ * for those set via php_value/php_admin_value.
+ *
+ * It's fairly common for people to use php_value instead of php_flag,
+ * which can leave you with an 'off' setting giving a false positive
+ * for code that just takes the ini_get() return value as a boolean.
+ *
+ * To make things extra interesting, setting via php_value accepts
+ * "true" and "yes" as true, but php.ini and php_flag consider them false. :)
+ * Unrecognized values go false... again opposite PHP's own coercion
+ * from string to bool.
+ *
+ * Luckily, 'properly' set settings will always come back as '0' or '1',
+ * so we only have to worry about them and the 'improper' settings.
+ *
+ * I frickin' hate PHP... :P
+ *
+ * @param string $setting
+ * @return bool
+ */
+function wfIniGetBool( $setting ) {
+	$val = ini_get( $setting );
+	// 'on' and 'true' can't have whitespace around them, but '1' can.
+	return strtolower( $val ) == 'on'
+		|| strtolower( $val ) == 'true'
+		|| strtolower( $val ) == 'yes'
+		|| preg_match( "/^\s*[+-]?0*[1-9]/", $val ); // approx C atoi() function
+}
+
 /**
  * Execute a shell command, with time and memory limits mirrored from the PHP
  * configuration if supported.
@@ -1783,7 +1810,7 @@ function wfUrlProtocols() {
 function wfShellExec( $cmd, &$retval=null ) {
 	global $IP, $wgMaxShellMemory, $wgMaxShellFileSize;
 	
-	if( ini_get( 'safe_mode' ) ) {
+	if( wfIniGetBool( 'safe_mode' ) ) {
 		wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" );
 		$retval = 1;
 		return "Unable to run external programs in safe mode.";
@@ -1807,10 +1834,12 @@ function wfShellExec( $cmd, &$retval=null ) {
 	}
 	wfDebug( "wfShellExec: $cmd\n" );
 	
-	$output = array();
 	$retval = 1; // error by default?
-	exec( $cmd, $output, $retval ); // returns the last line of output.
-	return implode( "\n", $output );
+	ob_start();
+	passthru( $cmd, $retval );
+	$output = ob_get_contents();
+	ob_end_clean();
+	return $output;
 	
 }
 
@@ -1901,8 +1930,18 @@ function wfRelativePath( $path, $from ) {
 	$path = str_replace( '/', DIRECTORY_SEPARATOR, $path );
 	$from = str_replace( '/', DIRECTORY_SEPARATOR, $from );
 	
+	// Trim trailing slashes -- fix for drive root
+	$path = rtrim( $path, DIRECTORY_SEPARATOR );
+	$from = rtrim( $from, DIRECTORY_SEPARATOR );
+	
 	$pieces  = explode( DIRECTORY_SEPARATOR, dirname( $path ) );
 	$against = explode( DIRECTORY_SEPARATOR, $from );
+	
+	if( $pieces[0] !== $against[0] ) {
+		// Non-matching Windows drive letters?
+		// Return a full path.
+		return $path;
+	}
 
 	// Trim off common prefix
 	while( count( $pieces ) && count( $against )
@@ -1922,13 +1961,35 @@ function wfRelativePath( $path, $from ) {
 	return implode( DIRECTORY_SEPARATOR, $pieces );
 }
 
+/**
+ * array_merge() does awful things with "numeric" indexes, including
+ * string indexes when happen to look like integers. When we want
+ * to merge arrays with arbitrary string indexes, we don't want our
+ * arrays to be randomly corrupted just because some of them consist
+ * of numbers.
+ *
+ * Fuck you, PHP. Fuck you in the ear!
+ *
+ * @param array $array1, [$array2, [...]]
+ * @return array
+ */
+function wfArrayMerge( $array1/* ... */ ) {
+	$out = $array1;
+	for( $i = 1; $i < func_num_args(); $i++ ) {
+		foreach( func_get_arg( $i ) as $key => $value ) {
+			$out[$key] = $value;
+		}
+	}
+	return $out;
+}
+
 /**
  * Make a URL index, appropriate for the el_index field of externallinks.
  */
 function wfMakeUrlIndex( $url ) {
 	global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php
-	$bits = parse_url( $url );
 	wfSuppressWarnings();
+	$bits = parse_url( $url );
 	wfRestoreWarnings();
 	if ( !$bits ) {
 		return false;
@@ -1952,13 +2013,19 @@ function wfMakeUrlIndex( $url ) {
 	// Reverse the labels in the hostname, convert to lower case
 	// For emails reverse domainpart only
 	if ( $bits['scheme'] == 'mailto' ) {
-		$mailparts = explode( '@', $bits['host'] );
-		$domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) );
+		$mailparts = explode( '@', $bits['host'], 2 );
+		if ( count($mailparts) === 2 ) {
+			$domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) );
+		} else {
+			// No domain specified, don't mangle it
+			$domainpart = '';
+		}
 		$reversedHost = $domainpart . '@' . $mailparts[0];
 	} else {
 		$reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) );
 	}
 	// Add an extra dot to the end
+	// Why? Is it in wrong place in mailto links?
 	if ( substr( $reversedHost, -1, 1 ) !== '.' ) {
 		$reversedHost .= '.';
 	}
@@ -2163,11 +2230,7 @@ function wfGetPrecompiledData( $name ) {
 function wfGetCaller( $level = 2 ) {
 	$backtrace = wfDebugBacktrace();
 	if ( isset( $backtrace[$level] ) ) {
-		if ( isset( $backtrace[$level]['class'] ) ) {
-			$caller = $backtrace[$level]['class'] . '::' . $backtrace[$level]['function'];
-		} else {
-			$caller = $backtrace[$level]['function'];
-		}
+		return wfFormatStackFrame($backtrace[$level]);
 	} else {
 		$caller = 'unknown';
 	}
@@ -2176,13 +2239,14 @@ function wfGetCaller( $level = 2 ) {
 
 /** Return a string consisting all callers in stack, somewhat useful sometimes for profiling specific points */
 function wfGetAllCallers() {
-	return implode('/', array_map(
-		create_function('$frame',' 
-			return isset( $frame["class"] )?
-				$frame["class"]."::".$frame["function"]:
-				$frame["function"]; 
-			'),
-		array_reverse(wfDebugBacktrace())));
+	return implode('/', array_map('wfFormatStackFrame',array_reverse(wfDebugBacktrace())));
+}
+
+/** Return a string representation of frame */
+function wfFormatStackFrame($frame) {
+	return isset( $frame["class"] )?
+		$frame["class"]."::".$frame["function"]:
+		$frame["function"];
 }
 
 /**
@@ -2247,7 +2311,7 @@ function &wfGetDB( $db = DB_LAST, $groups = array() ) {
  * @param mixed $title Title object or string. May be interwiki.
  * @param mixed $time Requested time for an archived image, or false for the 
  *                    current version. An image object will be returned which 
- *                    existed at or before the specified time.
+ *                    existed at the specified time.
  * @return File, or false if the file does not exist
  */
 function wfFindFile( $title, $time = false ) {
@@ -2320,4 +2384,24 @@ function wfGetNull() {
 	return wfIsWindows()
 		? 'NUL'
 		: '/dev/null';
-}
\ No newline at end of file
+}
+
+/**
+ * Displays a maxlag error
+ * 
+ * @param string $host Server that lags the most
+ * @param int $lag Maxlag (actual)
+ * @param int $maxLag Maxlag (requested)
+ */
+function wfMaxlagError( $host, $lag, $maxLag ) {
+	global $wgShowHostnames;
+	header( 'HTTP/1.1 503 Service Unavailable' );
+	header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
+	header( 'X-Database-Lag: ' . intval( $lag ) );
+	header( 'Content-Type: text/plain' );
+	if( $wgShowHostnames ) {
+		echo "Waiting for $host: $lag seconds lagged\n";
+	} else {
+		echo "Waiting for a database server: $lag seconds lagged\n";
+	}
+}
diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php
index 260439b2..050005dd 100644
--- a/includes/HTMLCacheUpdate.php
+++ b/includes/HTMLCacheUpdate.php
@@ -25,6 +25,7 @@ class HTMLCacheUpdate
 {
 	public $mTitle, $mTable, $mPrefix;
 	public $mRowsPerJob, $mRowsPerQuery;
+	public $mResult;
 
 	function __construct( $titleTo, $table ) {
 		global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
@@ -40,15 +41,14 @@ class HTMLCacheUpdate
 		$cond = $this->getToCondition();
 		$dbr = wfGetDB( DB_SLAVE );
 		$res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ );
-		$resWrap = new ResultWrapper( $dbr, $res );
+		$this->mResult = $res;
 		if ( $dbr->numRows( $res ) != 0 ) {
 			if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) {
-				$this->insertJobs( $resWrap );
+				$this->insertJobs( $res );
 			} else {
-				$this->invalidateIDs( $resWrap );
+				$this->invalidateIDs( $res );
 			}
 		}
-		$dbr->freeResult( $res );
 	}
 
 	function insertJobs( ResultWrapper $res ) {
@@ -87,6 +87,7 @@ class HTMLCacheUpdate
 			'imagelinks' => 'il',
 			'categorylinks' => 'cl',
 			'templatelinks' => 'tl',
+			'redirect' => 'rd',
 			
 			# Not needed
 			# 'externallinks' => 'el',
@@ -107,16 +108,14 @@ class HTMLCacheUpdate
 	}
 
 	function getToCondition() {
+		$prefix = $this->getPrefix();
 		switch ( $this->mTable ) {
 			case 'pagelinks':
-				return array( 
-					'pl_namespace' => $this->mTitle->getNamespace(),
-					'pl_title' => $this->mTitle->getDBkey()
-				);
 			case 'templatelinks':
-				return array(
-					'tl_namespace' => $this->mTitle->getNamespace(),
-					'tl_title' => $this->mTitle->getDBkey()
+			case 'redirect':
+				return array( 
+					"{$prefix}_namespace" => $this->mTitle->getNamespace(),
+					"{$prefix}_title" => $this->mTitle->getDBkey()
 				);
 			case 'imagelinks':
 				return array( 'il_to' => $this->mTitle->getDBkey() );
@@ -218,7 +217,6 @@ class HTMLCacheUpdateJob extends Job {
 		$dbr = wfGetDB( DB_SLAVE );
 		$res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ );
 		$update->invalidateIDs( new ResultWrapper( $dbr, $res ) );
-		$dbr->freeResult( $res );
 
 		return true;
 	}
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
index 64f266f6..46ecd169 100644
--- a/includes/ImageGallery.php
+++ b/includes/ImageGallery.php
@@ -303,7 +303,7 @@ class ImageGallery
 				$s .= "\n\t<tr>";
 			}
 			$s .=
-				"\n\t\t" . '<td><div class="gallerybox" style="width: '.($this->mWidths*1.25).'px;">'
+				"\n\t\t" . '<td><div class="gallerybox" style="width: '.($this->mWidths+35).'px;">'
 					. $thumbhtml
 					. "\n\t\t\t" . '<div class="gallerytext">' . "\n"
 						. $textlink . $text . $nb
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index 3cf6d0ac..573bc4d7 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -19,11 +19,14 @@ class ImagePage extends Article {
 	/* private */ var $repo;
 	var $mExtraDescription = false;
 
-	function __construct( $title ) {
+	function __construct( $title, $time = false ) {
 		parent::__construct( $title );
-		$this->img = wfFindFile( $this->mTitle );
+		$this->img = wfFindFile( $this->mTitle, $time );
 		if ( !$this->img ) {
 			$this->img = wfLocalFile( $this->mTitle );
+			$this->current = $this->img;
+		} else {
+			$this->current = $time ? wfLocalFile( $this->mTitle ) : $this->img;
 		}
 		$this->repo = $this->img->repo;
 	}
@@ -66,14 +69,14 @@ class ImagePage extends Article {
 		} else {
 			# Just need to set the right headers
 			$wgOut->setArticleFlag( true );
-			$wgOut->setRobotpolicy( 'index,follow' );
+			$wgOut->setRobotpolicy( 'noindex,nofollow' );
 			$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
 			$this->viewUpdates();
 		}
 
 		# Show shared description, if needed
 		if ( $this->mExtraDescription ) {
-			$fol = wfMsg( 'shareddescriptionfollows' );
+			$fol = wfMsgNoTrans( 'shareddescriptionfollows' );
 			if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) {
 				$wgOut->addWikiText( $fol );
 			}
@@ -157,7 +160,7 @@ class ImagePage extends Article {
 	}
 
 	function openShowImage() {
-		global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang;
+		global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang;
 
 		$full_url  = $this->img->getURL();
 		$linkAttribs = false;
@@ -176,6 +179,7 @@ class ImagePage extends Article {
 		$maxWidth = $max[0];
 		$maxHeight = $max[1];
 		$sk = $wgUser->getSkin();
+		$dirmark = $wgContLang->getDirMark();
 
 		if ( $this->img->exists() ) {
 			# image
@@ -219,7 +223,7 @@ class ImagePage extends Article {
 					}
 					$msgbig  = wfMsgHtml( 'show-big-image' );
 					$msgsmall = wfMsgExt( 'show-big-image-thumb',
-						array( 'parseinline' ), $width, $height );
+						array( 'parseinline' ), $wgLang->formatNum( $width ), $wgLang->formatNum( $height ) );
 				} else {
 					# Image is small enough to show full size on image page
 					$msgbig = htmlspecialchars( $this->img->getName() );
@@ -235,7 +239,7 @@ class ImagePage extends Article {
 				} else {
 					$anchorclose .= 
 						$msgsmall .
-						'<br />' . Xml::tags( 'a', $linkAttribs,  $msgbig ) . ' ' . $longDesc;
+						'<br />' . Xml::tags( 'a', $linkAttribs,  $msgbig ) . "$dirmark " . $longDesc;
 				}
 
 				if ( $this->img->isMultipage() ) {
@@ -308,10 +312,8 @@ class ImagePage extends Article {
 			if ($showLink) {
 				$filename = wfEscapeWikiText( $this->img->getName() );
 
-				global $wgContLang;
-				$dirmark = $wgContLang->getDirMark();
 				if (!$this->img->isSafeFile()) {
-					$warning = wfMsg( 'mediawarning' );
+					$warning = wfMsgNoTrans( 'mediawarning' );
 					$wgOut->addWikiText( <<<EOT
 <div class="fullMedia">
 <span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark
@@ -364,9 +366,8 @@ EOT
 	}
 
 	function getUploadUrl() {
-		global $wgServer;
 		$uploadTitle = SpecialPage::getTitleFor( 'Upload' );
-		return $wgServer . $uploadTitle->getLocalUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) );
+		return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) );
 	}
 
 	/**
@@ -412,25 +413,23 @@ EOT
 
 		$sk = $wgUser->getSkin();
 
-		$line = $this->img->nextHistoryLine();
-
-		if ( $line ) {
-			$list = new ImageHistoryList( $sk, $this->img );
-			$file = $this->repo->newFileFromRow( $line );
+		if ( $this->img->exists() ) {
+			$list = new ImageHistoryList( $sk, $this->current );
+			$file = $this->current;
 			$dims = $file->getDimensionsString();
 			$s = $list->beginImageHistoryList() .
-				$list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp),
-					$this->mTitle->getDBkey(),  $line->img_user,
-					$line->img_user_text, $line->img_size, $line->img_description,
+				$list->imageHistoryLine( true, wfTimestamp(TS_MW, $file->getTimestamp()),
+					$this->mTitle->getDBkey(),  $file->getUser('id'),
+					$file->getUser('text'), $file->getSize(), $file->getDescription(),
 					$dims
 				);
 
-			while ( $line = $this->img->nextHistoryLine() ) {
-				$file = $this->repo->newFileFromRow( $line );
+			$hist = $this->img->getHistory();
+			foreach( $hist as $file ) {
 				$dims = $file->getDimensionsString();
-				$s .= $list->imageHistoryLine( false, $line->oi_timestamp,
-			  		$line->oi_archive_name, $line->oi_user,
-			  		$line->oi_user_text, $line->oi_size, $line->oi_description,
+				$s .= $list->imageHistoryLine( false, wfTimestamp(TS_MW, $file->getTimestamp()),
+			  		$file->getArchiveName(), $file->getUser('id'),
+			  		$file->getUser('text'), $file->getSize(), $file->getDescription(),
 					$dims
 				);
 			}
@@ -563,6 +562,19 @@ class ImageHistoryList {
 		return "</table>\n";
 	}
 
+	/**
+	 * Create one row of file history
+	 *
+	 * @param bool $iscur is this the current file version?
+	 * @param string $timestamp timestamp of file version
+	 * @param string $img filename
+	 * @param int $user ID of uploading user
+	 * @param string $usertext username of uploading user
+	 * @param int $size size of file version
+	 * @param string $description description of file version
+	 * @param string $dims dimensions of file version
+	 * @return string a HTML formatted table row
+	 */
 	public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) {
 		global $wgUser, $wgLang, $wgContLang;
 		$local = $this->img->isLocal();
@@ -575,28 +587,28 @@ class ImageHistoryList {
 			$q[] = 'action=delete';
 			if( !$iscur )
 				$q[] = 'oldimage=' . urlencode( $img );
-			$row .= '(' . $this->skin->makeKnownLinkObj(
+			$row .= $this->skin->makeKnownLinkObj(
 				$this->title,
 				wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
 				implode( '&', $q )
-			) . ')';
+			);
 			$row .= '</td>';
 		}
 
 		// Reversion link/current indicator
 		$row .= '<td>';
 		if( $iscur ) {
-			$row .= '(' . wfMsgHtml( 'filehist-current' ) . ')';
+			$row .= wfMsgHtml( 'filehist-current' );
 		} elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
 			$q = array();
 			$q[] = 'action=revert';
 			$q[] = 'oldimage=' . urlencode( $img );
 			$q[] = 'wpEditToken=' . urlencode( $wgUser->editToken( $img ) );
-			$row .= '(' . $this->skin->makeKnownLinkObj(
+			$row .= $this->skin->makeKnownLinkObj(
 				$this->title,
 				wfMsgHtml( 'filehist-revert' ),
 				implode( '&', $q )
-			) . ')';
+			);
 		}
 		$row .= '</td>';
 
diff --git a/includes/JobQueue.php b/includes/JobQueue.php
index a2780bdb..5cec3106 100644
--- a/includes/JobQueue.php
+++ b/includes/JobQueue.php
@@ -4,8 +4,6 @@ if ( !defined( 'MEDIAWIKI' ) ) {
 	die( "This file is part of MediaWiki, it is not a valid entry point\n" );
 }
 
-require_once('UserMailer.php');
-
 /**
  * Class to both describe a background job and handle jobs.
  */
@@ -290,3 +288,4 @@ abstract class Job {
 	}
 }
 
+
diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php
index 20bcd3d4..db1114c9 100644
--- a/includes/LinkBatch.php
+++ b/includes/LinkBatch.php
@@ -34,7 +34,7 @@ class LinkBatch {
 			$this->data[$ns] = array();
 		}
 
-		$this->data[$ns][$dbkey] = 1;
+		$this->data[$ns][str_replace( ' ', '_', $dbkey )] = 1;
 	}
 
 	/**
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
index ee668f08..ced76d75 100644
--- a/includes/LinkFilter.php
+++ b/includes/LinkFilter.php
@@ -51,6 +51,7 @@ class LinkFilter {
 	 * @param $prot        String: protocol
 	 */
 	 public static function makeLike( $filterEntry , $prot = 'http://' ) {
+		$db = wfGetDB( DB_MASTER );
 		if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
 			$subdomains = true;
 			$filterEntry = substr( $filterEntry, 2 );
@@ -83,23 +84,23 @@ class LinkFilter {
 			$mailparts = explode( '@', $host );
 			$domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) );
 			$host = $domainpart . '@' . $mailparts[0];
-			$like = "$prot$host%";
+			$like = $db->escapeLike( "$prot$host" ) . "%";
 		} elseif ( $prot == 'mailto:' ) {
 			// domainpart of email adress only. do not add '.'
 			$host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );	
-			$like = "$prot$host%";			
+			$like = $db->escapeLike( "$prot$host" ) . "%";			
 		} else {
 			$host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );	
 			if ( substr( $host, -1, 1 ) !== '.' ) {
 				$host .= '.';
 			}
-			$like = "$prot$host";
+			$like = $db->escapeLike( "$prot$host" );
 
 			if ( $subdomains ) {
 				$like .= '%';
 			}
 			if ( !$subdomains || $path !== '/' ) {
-				$like .= $path . '%';
+				$like .= $db->escapeLike( $path ) . '%';
 			}
 		}
 		return $like;
diff --git a/includes/Linker.php b/includes/Linker.php
index 9397b800..4b092cf9 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -52,19 +52,11 @@ class Linker {
 	}
 
 	/** @todo document */
-	function getInternalLinkAttributes( $link, $text, $broken = false ) {
+	function getInternalLinkAttributes( $link, $text, $class='' ) {
 		$link = urldecode( $link );
 		$link = str_replace( '_', ' ', $link );
 		$link = htmlspecialchars( $link );
-
-		if( $broken == 'stub' ) {
-			$r = ' class="stub"';
-		} else if ( $broken == 'yes' ) {
-			$r = ' class="new"';
-		} else {
-			$r = '';
-		}
-
+		$r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : '';
 		$r .= " title=\"{$link}\"";
 		return $r;
 	}
@@ -72,21 +64,37 @@ class Linker {
 	/**
 	 * @param $nt Title object.
 	 * @param $text String: FIXME
-	 * @param $broken Boolean: FIXME, default 'false'.
+	 * @param $class String: CSS class of the link, default ''.
 	 */
-	function getInternalLinkAttributesObj( &$nt, $text, $broken = false ) {
-		if( $broken == 'stub' ) {
-			$r = ' class="stub"';
-		} else if ( $broken == 'yes' ) {
-			$r = ' class="new"';
-		} else {
-			$r = '';
-		}
-
+	function getInternalLinkAttributesObj( &$nt, $text, $class='' ) {
+		$r = ($class != '') ? ' class="' . htmlspecialchars( $class ) . '"' : '';
 		$r .= ' title="' . $nt->getEscapedText() . '"';
 		return $r;
 	}
 
+	/**
+	 * Return the CSS colour of a known link
+	 *
+	 * @param mixed $s
+	 * @param integer $threshold user defined threshold
+	 * @return string CSS class
+	 */
+	function getLinkColour( $s, $threshold ) {
+		if( $s === false ) {
+			return '';
+		}
+
+		$colour = '';
+		if ( !empty( $s->page_is_redirect ) ) {
+			# Page is a redirect
+			$colour = 'mw-redirect';
+		} elseif ( $threshold > 0 && $s->page_len < $threshold && Namespace::isContent( $s->page_namespace ) ) {
+			# Page is a stub
+			$colour = 'stub';
+		}
+		return $colour;
+	}
+
 	/**
 	 * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call
 	 * it if you already have a title object handy. See makeLinkObj for further documentation.
@@ -99,16 +107,16 @@ class Linker {
 	 *                      the end of the link.
 	 */
 	function makeLink( $title, $text = '', $query = '', $trail = '' ) {
-		wfProfileIn( 'Linker::makeLink' );
+		wfProfileIn( __METHOD__ );
 	 	$nt = Title::newFromText( $title );
-		if ($nt) {
+		if ( $nt instanceof Title ) {
 			$result = $this->makeLinkObj( $nt, $text, $query, $trail );
 		} else {
 			wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" );
 			$result = $text == "" ? $title : $text;
 		}
 
-		wfProfileOut( 'Linker::makeLink' );
+		wfProfileOut( __METHOD__ );
 		return $result;
 	}
 
@@ -125,8 +133,8 @@ class Linker {
 	 */
 	function makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') {
 		$nt = Title::newFromText( $title );
-		if ($nt) {
-			return $this->makeKnownLinkObj( Title::newFromText( $title ), $text, $query, $trail, $prefix , $aprops );
+		if ( $nt instanceof Title ) {
+			return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix , $aprops );
 		} else {
 			wfDebug( 'Invalid title passed to Linker::makeKnownLink(): "'.$title."\"\n" );
 			return $text == '' ? $title : $text;
@@ -146,8 +154,8 @@ class Linker {
 	 */
 	function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) {
 		$nt = Title::newFromText( $title );
-		if ($nt) {
-			return $this->makeBrokenLinkObj( Title::newFromText( $title ), $text, $query, $trail );
+		if ( $nt instanceof Title ) {
+			return $this->makeBrokenLinkObj( $nt, $text, $query, $trail );
 		} else {
 			wfDebug( 'Invalid title passed to Linker::makeBrokenLink(): "'.$title."\"\n" );
 			return $text == '' ? $title : $text;
@@ -155,6 +163,8 @@ class Linker {
 	}
 
 	/**
+	 * @deprecated use makeColouredLinkObj
+	 * 
 	 * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call
 	 * it if you already have a title object handy. See makeStubLinkObj for further documentation.
 	 * 
@@ -167,8 +177,8 @@ class Linker {
 	 */
 	function makeStubLink( $title, $text = '', $query = '', $trail = '' ) {
 		$nt = Title::newFromText( $title );
-		if ($nt) {
-			return $this->makeStubLinkObj( Title::newFromText( $title ), $text, $query, $trail );
+		if ( $nt instanceof Title ) {
+			return $this->makeStubLinkObj( $nt, $text, $query, $trail );
 		} else {
 			wfDebug( 'Invalid title passed to Linker::makeStubLink(): "'.$title."\"\n" );
 			return $text == '' ? $title : $text;
@@ -191,13 +201,11 @@ class Linker {
 	 */
 	function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
 		global $wgUser;
-		$fname = 'Linker::makeLinkObj';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
-		# Fail gracefully
-		if ( ! is_object($nt) ) {
-			# throw new MWException();
-			wfProfileOut( $fname );
+		if ( !$nt instanceof Title ) {
+			# Fail gracefully
+			wfProfileOut( __METHOD__ );
 			return "<!-- ERROR -->{$prefix}{$text}{$trail}";
 		}
 
@@ -217,23 +225,23 @@ class Linker {
 			}
 			$t = "<a href=\"{$u}\"{$style}>{$text}{$inside}</a>";
 
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
 			return $t;
 		} elseif ( $nt->isAlwaysKnown() ) {
 			# Image links, special page links and self-links with fragements are always known.
 			$retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
 		} else {
-			wfProfileIn( $fname.'-immediate' );
+			wfProfileIn( __METHOD__.'-immediate' );
 
-			# Handles links to special pages wich do not exist in the database:
+			# Handles links to special pages which do not exist in the database:
 			if( $nt->getNamespace() == NS_SPECIAL ) {
-				if( SpecialPage::exists( $nt->getDbKey() ) ) {
+				if( SpecialPage::exists( $nt->getDBkey() ) ) {
 					$retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
 				} else {
 					$retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
 				}
-				wfProfileOut( $fname.'-immediate' );
-				wfProfileOut( $fname );
+				wfProfileOut( __METHOD__.'-immediate' );
+				wfProfileOut( __METHOD__ );
 				return $retVal;
 			}
 
@@ -242,29 +250,23 @@ class Linker {
 			if ( 0 == $aid ) {
 				$retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix );
 			} else {
-				$stub = false;
+				$colour = '';
 				if ( $nt->isContentPage() ) {
+					# FIXME: This is stupid, we should combine this query with
+					# the Title::getArticleID() query above.
 					$threshold = $wgUser->getOption('stubthreshold');
-					if ( $threshold > 0 ) {
-						$dbr = wfGetDB( DB_SLAVE );
-						$s = $dbr->selectRow(
-							array( 'page' ),
-							array( 'page_len',
-							       'page_is_redirect' ),
-							array( 'page_id' => $aid ), $fname ) ;
-						$stub = ( $s !== false && !$s->page_is_redirect &&
-							  $s->page_len < $threshold );
-					}
-				}
-				if ( $stub ) {
-					$retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix );
-				} else {
-					$retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
+					$dbr = wfGetDB( DB_SLAVE );
+					$s = $dbr->selectRow(
+						array( 'page' ),
+						array( 'page_len', 'page_is_redirect', 'page_namespace' ),
+						array( 'page_id' => $aid ), __METHOD__ ) ;
+					$colour = $this->getLinkColour( $s, $threshold );
 				}
+				$retVal = $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
 			}
-			wfProfileOut( $fname.'-immediate' );
+			wfProfileOut( __METHOD__.'-immediate' );
 		}
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $retVal;
 	}
 
@@ -283,13 +285,12 @@ class Linker {
 	 * @return the a-element
 	 */
 	function makeKnownLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
+		wfProfileIn( __METHOD__ );
 
-		$fname = 'Linker::makeKnownLinkObj';
-		wfProfileIn( $fname );
-
-		if ( !is_object( $nt ) ) {
-			wfProfileOut( $fname );
-			return $text;
+		if ( !$nt instanceof Title ) {
+			# Fail gracefully
+			wfProfileOut( __METHOD__ );
+			return "<!-- ERROR -->{$prefix}{$text}{$trail}";
 		}
 
 		$u = $nt->escapeLocalURL( $query );
@@ -313,14 +314,14 @@ class Linker {
 
 		list( $inside, $trail ) = Linker::splitTrail( $trail );
 		$r = "<a href=\"{$u}\"{$style}{$aprops}>{$prefix}{$text}{$inside}</a>{$trail}";
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $r;
 	}
 
 	/**
 	 * Make a red link to the edit page of a given title.
 	 * 
-	 * @param $title String: The text of the title
+	 * @param $nt Title object of the target page
 	 * @param $text  String: Link text
 	 * @param $query String: Optional query part
 	 * @param $trail String: Optional trail. Alphabetic characters at the start of this string will
@@ -328,15 +329,14 @@ class Linker {
 	 *                      the end of the link.
 	 */
 	function makeBrokenLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-		# Fail gracefully
-		if ( ! isset($nt) ) {
-			# throw new MWException();
+		wfProfileIn( __METHOD__ );
+
+		if ( !$nt instanceof Title ) {
+			# Fail gracefully
+			wfProfileOut( __METHOD__ );
 			return "<!-- ERROR -->{$prefix}{$text}{$trail}";
 		}
 
-		$fname = 'Linker::makeBrokenLinkObj';
-		wfProfileIn( $fname );
-
 		if( $nt->getNamespace() == NS_SPECIAL ) {
 			$q = $query;
 		} else if ( '' == $query ) {
@@ -349,19 +349,21 @@ class Linker {
 		if ( '' == $text ) {
 			$text = htmlspecialchars( $nt->getPrefixedText() );
 		}
-		$style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" );
+		$style = $this->getInternalLinkAttributesObj( $nt, $text, 'new' );
 
 		list( $inside, $trail ) = Linker::splitTrail( $trail );
 		$s = "<a href=\"{$u}\"{$style}>{$prefix}{$text}{$inside}</a>{$trail}";
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $s;
 	}
 
 	/**
+	 * @deprecated use makeColouredLinkObj
+	 * 
 	 * Make a brown link to a short article.
 	 * 
-	 * @param $title String: the text of the title
+	 * @param $nt Title object of the target page
 	 * @param $text  String: link text
 	 * @param $query String: optional query part
 	 * @param $trail String: optional trail. Alphabetic characters at the start of this string will
@@ -369,7 +371,25 @@ class Linker {
 	 *                      the end of the link.
 	 */
 	function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-		$style = $this->getInternalLinkAttributesObj( $nt, $text, 'stub' );
+		return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix );
+	}
+
+	/**
+	 * Make a coloured link.
+	 * 
+	 * @param $nt Title object of the target page
+	 * @param $colour Integer: colour of the link
+	 * @param $text   String:  link text
+	 * @param $query  String:  optional query part
+	 * @param $trail  String:  optional trail. Alphabetic characters at the start of this string will
+	 *                      be included in the link text. Other characters will be appended after
+	 *                      the end of the link.
+	 */
+	function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
+
+		if($colour != ''){
+			$style = $this->getInternalLinkAttributesObj( $nt, $text, $colour );
+		} else $style = '';
 		return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix, '', $style );
 	}
 
@@ -388,11 +408,8 @@ class Linker {
 	function makeSizeLinkObj( $size, $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
 		global $wgUser;
 		$threshold = intval( $wgUser->getOption( 'stubthreshold' ) );
-		if( $size < $threshold ) {
-			return $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix );
-		} else {
-			return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix );
-		}
+		$colour = ( $size < $threshold ) ? 'stub' : '';
+		return $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
 	}
 
 	/** 
@@ -446,6 +463,7 @@ class Linker {
 	 * @param boolean $thumb shows image as thumbnail in a frame
 	 * @param string $manualthumb image name for the manual thumbnail
 	 * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom
+	 * @param string $time, timestamp of the file, set as false for current
 	 * @return string
 	 */
 	function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false,
@@ -468,7 +486,7 @@ class Linker {
 			$frameParams['valign'] = $valign;
 		}
 		$file = wfFindFile( $title, $time );
-		return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams );
+		return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams, $time );
 	}
 
 	/**
@@ -476,26 +494,27 @@ class Linker {
 	 * @param Title $title Title object
 	 * @param File $file File object, or false if it doesn't exist
 	 *
-     * @param array $frameParams Associative array of parameters external to the media handler.
-     *     Boolean parameters are indicated by presence or absence, the value is arbitrary and 
-     *     will often be false.
-     *          thumbnail       If present, downscale and frame
-     *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
-     *          framed          Shows image in original size in a frame
-     *          frameless       Downscale but don't frame
-     *          upright         If present, tweak default sizes for portrait orientation
-     *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
-     *          border          If present, show a border around the image
-     *          align           Horizontal alignment (left, right, center, none)
-     *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle, 
-     *                          bottom, text-bottom)
-     *          alt             Alternate text for image (i.e. alt attribute). Plain text.
-     *          caption         HTML for image caption.
+	 * @param array $frameParams Associative array of parameters external to the media handler.
+	 *     Boolean parameters are indicated by presence or absence, the value is arbitrary and 
+	 *     will often be false.
+	 *          thumbnail       If present, downscale and frame
+	 *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
+	 *          framed          Shows image in original size in a frame
+	 *          frameless       Downscale but don't frame
+	 *          upright         If present, tweak default sizes for portrait orientation
+	 *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
+	 *          border          If present, show a border around the image
+	 *          align           Horizontal alignment (left, right, center, none)
+	 *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle, 
+	 *                          bottom, text-bottom)
+	 *          alt             Alternate text for image (i.e. alt attribute). Plain text.
+	 *          caption         HTML for image caption.
 	 *
-     * @param array $handlerParams Associative array of media handler parameters, to be passed 
-     *       to transform(). Typical keys are "width" and "page". 
+	 * @param array $handlerParams Associative array of media handler parameters, to be passed 
+	 *       to transform(). Typical keys are "width" and "page". 
+	 * @param string $time, timestamp of the file, set as false for current
 	 */
-	function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) {
+	function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) {
 		global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright;
 		if ( $file && !$file->allowInlineDisplay() ) {
 			wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" );
@@ -556,7 +575,16 @@ class Linker {
 			if ( $fp['align'] == '' ) {
 				$fp['align'] = $wgContLang->isRTL() ? 'left' : 'right';
 			}
-			return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp ).$postfix;
+			return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time ).$postfix;
+		}
+
+		if ( $file && isset( $fp['frameless'] ) ) {
+			$srcWidth = $file->getWidth( $page );
+			# For "frameless" option: do not present an image bigger than the source (for bitmap-style images)
+			# This is the same behaviour as the "thumb" option does it already.
+			if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
+				$hp['width'] = $srcWidth;
+			}
 		}
 
 		if ( $file && $hp['width'] ) {
@@ -567,7 +595,7 @@ class Linker {
 		}
 
 		if ( !$thumb ) {
-			$s = $this->makeBrokenImageLinkObj( $title );
+			$s = $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true );
 		} else {
 			$s = $thumb->toHtml( array(
 				'desc-link' => true,
@@ -597,7 +625,7 @@ class Linker {
 		return $this->makeThumbLink2( $title, $file, $frameParams, $params );
 	}
 
-	function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) {
+	function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array(), $time = false ) {
 		global $wgStylePath, $wgContLang;
 		$exists = $file && $file->exists();
 
@@ -654,12 +682,10 @@ class Linker {
 		$url = $title->getLocalURL( $query );
 
 		$more = htmlspecialchars( wfMsg( 'thumbnail-more' ) );
-		$magnifyalign = $wgContLang->isRTL() ? 'left' : 'right';
-		$textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : '';
 
 		$s = "<div class=\"thumb t{$fp['align']}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
 		if( !$exists ) {
-			$s .= $this->makeBrokenImageLinkObj( $title );
+			$s .= $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true );
 			$zoomicon = '';
 		} elseif ( !$thumb ) {
 			$s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) );
@@ -672,13 +698,13 @@ class Linker {
 			if ( isset( $fp['framed'] ) ) {
 				$zoomicon="";
 			} else {
-				$zoomicon =  '<div class="magnify" style="float:'.$magnifyalign.'">'.
+				$zoomicon =  '<div class="magnify">'.
 					'<a href="'.$url.'" class="internal" title="'.$more.'">'.
 					'<img src="'.$wgStylePath.'/common/images/magnify-clip.png" ' .
 					'width="15" height="11" alt="" /></a></div>';
 			}
 		}
-		$s .= '  <div class="thumbcaption"'.$textalign.'>'.$zoomicon.$fp['caption']."</div></div></div>";
+		$s .= '  <div class="thumbcaption">'.$zoomicon.$fp['caption']."</div></div></div>";
 		return str_replace("\n", ' ', $s);
 	}
 
@@ -690,21 +716,27 @@ class Linker {
 	 * @param string $query Query string
 	 * @param string $trail Link trail
 	 * @param string $prefix Link prefix
+	 * @param bool $time, a file of a certain timestamp was requested
 	 * @return string
 	 */
-	public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
+	public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '', $time = false ) {
 		global $wgEnableUploads;
 		if( $title instanceof Title ) {
 			wfProfileIn( __METHOD__ );
-			if( $wgEnableUploads ) {
+			$currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
+			if( $wgEnableUploads && !$currentExists ) {
 				$upload = SpecialPage::getTitleFor( 'Upload' );
 				if( $text == '' )
 					$text = htmlspecialchars( $title->getPrefixedText() );
+				$redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
+				if( $redir ) {	
+					return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix );
+				} 
 				$q = 'wpDestFile=' . $title->getPartialUrl();
 				if( $query != '' )
 					$q .= '&' . $query;
 				list( $inside, $trail ) = self::splitTrail( $trail );
-				$style = $this->getInternalLinkAttributesObj( $title, $text, 'yes' );
+				$style = $this->getInternalLinkAttributesObj( $title, $text, 'new' );
 				wfProfileOut( __METHOD__ );
 				return '<a href="' . $upload->escapeLocalUrl( $q ) . '"'
 					. $style . '>' . $prefix . $text . $inside . '</a>' . $trail;
@@ -744,7 +776,7 @@ class Linker {
 				$class = 'internal';
 			} else {
 				$upload = SpecialPage::getTitleFor( 'Upload' );
-				$url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDbKey() ) );
+				$url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDBkey() ) );
 				$class = 'new';
 			}
 			$alt = htmlspecialchars( $title->getText() );
@@ -946,8 +978,9 @@ class Linker {
 	 * add a separator where needed and format the comment itself with CSS
 	 * Called by Linker::formatComment.
 	 *
-	 * @param $comment Comment text
-	 * @param $title An optional title object used to links to sections
+	 * @param string $comment Comment text
+	 * @param object $title An optional title object used to links to sections
+	 * @return string $comment formatted comment
 	 *
 	 * @todo Document the $local parameter.
 	 */
@@ -975,14 +1008,17 @@ class Linker {
 					$sectionTitle = wfClone( $title );
 					$sectionTitle->mFragment = $section;
 				}
-				$link = $this->makeKnownLinkObj( $sectionTitle, wfMsg( 'sectionlink' ) );
+				$link = $this->makeKnownLinkObj( $sectionTitle, wfMsgForContent( 'sectionlink' ) );
+			}
+			$auto = $link . $auto;
+			if( $pre ) {
+				$auto = '- ' . $auto; # written summary $presep autocomment (summary /* section */)
 			}
-			$sep='-';
-			$auto=$link.$auto;
-			if($pre) { $auto = $sep.' '.$auto; }
-			if($post) { $auto .= ' '.$sep; }
-			$auto='<span class="autocomment">'.$auto.'</span>';
-			$comment=$pre.$auto.$post;
+			if( $post ) {
+				$auto .= ': '; # autocomment $postsep written summary (/* section */ summary)
+			}
+			$auto = '<span class="autocomment">' . $auto . '</span>';
+			$comment = $pre . $auto . $post;
 		}
 
 		return $comment;
@@ -992,42 +1028,49 @@ class Linker {
 	 * Formats wiki links and media links in text; all other wiki formatting
 	 * is ignored
 	 *
+	 * @fixme doesn't handle sub-links as in image thumb texts like the main parser
 	 * @param string $comment Text to format links in
 	 * @return string
 	 */
 	public function formatLinksInComment( $comment ) {
+		return preg_replace_callback(
+			'/\[\[:?(.*?)(\|(.*?))*\]\]([^[]*)/',
+			array( $this, 'formatLinksInCommentCallback' ),
+			$comment );
+	}
+	
+	protected function formatLinksInCommentCallback( $match ) {
 		global $wgContLang;
 
 		$medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
 		$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
+		
+		$comment = $match[0];
 
-		$match = array();
-		while(preg_match('/\[\[:?(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) {
-			# Handle link renaming [[foo|text]] will show link as "text"
-			if( "" != $match[3] ) {
-				$text = $match[3];
-			} else {
-				$text = $match[1];
-			}
-			$submatch = array();
-			if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
-				# Media link; trail not supported.
-				$linkRegexp = '/\[\[(.*?)\]\]/';
-				$thelink = $this->makeMediaLink( $submatch[1], "", $text );
+		# Handle link renaming [[foo|text]] will show link as "text"
+		if( "" != $match[3] ) {
+			$text = $match[3];
+		} else {
+			$text = $match[1];
+		}
+		$submatch = array();
+		if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
+			# Media link; trail not supported.
+			$linkRegexp = '/\[\[(.*?)\]\]/';
+			$thelink = $this->makeMediaLink( $submatch[1], "", $text );
+		} else {
+			# Other kind of link
+			if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) {
+				$trail = $submatch[1];
 			} else {
-				# Other kind of link
-				if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) {
-					$trail = $submatch[1];
-				} else {
-					$trail = "";
-				}
-				$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
-				if (isset($match[1][0]) && $match[1][0] == ':')
-					$match[1] = substr($match[1], 1);
-				$thelink = $this->makeLink( $match[1], $text, "", $trail );
+				$trail = "";
 			}
-			$comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 );
+			$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
+			if (isset($match[1][0]) && $match[1][0] == ':')
+				$match[1] = substr($match[1], 1);
+			$thelink = $this->makeLink( $match[1], $text, "", $trail );
 		}
+		$comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 );
 
 		return $comment;
 	}
@@ -1103,7 +1146,7 @@ class Linker {
 	/** @todo document */
 	function tocList($toc) {
 		global $wgJsMimeType;
-		$title =  wfMsgHtml('toc') ;
+		$title = wfMsgHtml('toc') ;
 		return
 		   '<table id="toc" class="toc" summary="' . $title .'"><tr><td>'
 		 . '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
@@ -1167,9 +1210,9 @@ class Linker {
 
 		// The two hooks have slightly different interfaces . . .
 		if( $hook == 'EditSectionLink' ) {
-			wfRunHooks( $hook, array( &$this, $nt, $section, $hint, $url, &$result ) );
+			wfRunHooks( 'EditSectionLink', array( &$this, $nt, $section, $hint, $url, &$result ) );
 		} elseif( $hook == 'EditSectionLinkForOther' ) {
-			wfRunHooks( $hook, array( &$this, $nt, $section, $url, &$result ) );
+			wfRunHooks( 'EditSectionLinkForOther', array( &$this, $nt, $section, $url, &$result ) );
 		}
 		
 		// For reverse compatibility, add the brackets *after* the hook is run,
@@ -1334,6 +1377,8 @@ class Linker {
 	 *   element (e.g., ' title="This does something [x]" accesskey="x"').
 	 */
 	public function tooltipAndAccesskey($name) {
+		$fname="Linker::tooltipAndAccesskey";
+		wfProfileIn($fname);
 		$out = '';
 
 		$tooltip = wfMsg('tooltip-'.$name);
@@ -1349,6 +1394,7 @@ class Linker {
 		} elseif ($out) {
 			$out .= '"';
 		}
+		wfProfileOut($fname);
 		return $out;
 	}
 
@@ -1373,7 +1419,3 @@ class Linker {
 		return $out;
 	}
 }
-
-
-
-
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index 9bcd9d67..a52414c3 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -73,11 +73,15 @@ class LinksUpdate {
 	 */
 	function doUpdate() {
 		global $wgUseDumbLinkUpdate;
+		
+		wfRunHooks( 'LinksUpdate', array( &$this ) );
 		if ( $wgUseDumbLinkUpdate ) {
 			$this->doDumbUpdate();
 		} else {
 			$this->doIncrementalUpdate();
 		}
+		wfRunHooks( 'LinksUpdateComplete', array( &$this ) );
+
 	}
 
 	function doIncrementalUpdate() {
@@ -595,5 +599,12 @@ class LinksUpdate {
 		}
 		return $arr;
 	}
+	
+	/**
+	 * Return the title object of the page being updated
+	 */	
+	function getTitle() {
+		return $this->mTitle;
+	}
 }
 
diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php
index 65a6d5a6..0cdadd1e 100644
--- a/includes/LoadBalancer.php
+++ b/includes/LoadBalancer.php
@@ -16,12 +16,6 @@ class LoadBalancer {
 	/* private */ var $mWaitForFile, $mWaitForPos, $mWaitTimeout;
 	/* private */ var $mLaggedSlaveMode, $mLastError = 'Unknown error';
 
-	/**
-	 * Scale polling time so that under overload conditions, the database server
-	 * receives a SHOW STATUS query at an average interval of this many microseconds
-	 */
-	const AVG_STATUS_POLL = 2000;
-
 	function __construct( $servers, $failFunction = false, $waitTimeout = 10, $waitForMasterNow = false )
 	{
 		$this->mServers = $servers;
@@ -133,7 +127,7 @@ class LoadBalancer {
 	 * Side effect: opens connections to databases
 	 */
 	function getReaderIndex() {
-		global $wgReadOnly, $wgDBClusterTimeout;
+		global $wgReadOnly, $wgDBClusterTimeout, $wgDBAvgStatusPoll;
 
 		$fname = 'LoadBalancer::getReaderIndex';
 		wfProfileIn( $fname );
@@ -180,7 +174,7 @@ class LoadBalancer {
 								# Too much load, back off and wait for a while.
 								# The sleep time is scaled by the number of threads connected,
 								# to produce a roughly constant global poll rate.
-								$sleepTime = self::AVG_STATUS_POLL * $status['Threads_connected'];
+								$sleepTime = $wgDBAvgStatusPoll * $status['Threads_connected'];
 
 								# If we reach the timeout and exit the loop, don't use it
 								$i = false;
@@ -324,13 +318,13 @@ class LoadBalancer {
 
 		# Query groups
 		if ( !is_array( $groups ) ) {
-			$groupIndex = $this->getGroupIndex( $groups, $i );
+			$groupIndex = $this->getGroupIndex( $groups );
 			if ( $groupIndex !== false ) {
 				$i = $groupIndex;
 			}
 		} else {
 			foreach ( $groups as $group ) {
-				$groupIndex = $this->getGroupIndex( $group, $i );
+				$groupIndex = $this->getGroupIndex( $group );
 				if ( $groupIndex !== false ) {
 					$i = $groupIndex;
 					break;
@@ -432,8 +426,7 @@ class LoadBalancer {
 		return $db;
 	}
 
-	function reportConnectionError( &$conn )
-	{
+	function reportConnectionError( &$conn ) {
 		$fname = 'LoadBalancer::reportConnectionError';
 		wfProfileIn( $fname );
 		# Prevent infinite recursion
@@ -552,6 +545,17 @@ class LoadBalancer {
 			}
 		}
 	}
+	
+	/* Issue COMMIT only on master, only if queries were done on connection */
+	function commitMasterChanges() {
+		// Always 0, but who knows.. :)
+		$i = $this->getWriterIndex();
+		if (array_key_exists($i,$this->mConnections)) {
+			if ($this->mConnections[$i]->lastQuery() != '') {
+				$this->mConnections[$i]->immediateCommit();
+			}
+		}
+	}
 
 	function waitTimeout( $value = NULL ) {
 		return wfSetVar( $this->mWaitTimeout, $value );
diff --git a/includes/LogPage.php b/includes/LogPage.php
index 8982b59f..7c89df76 100644
--- a/includes/LogPage.php
+++ b/includes/LogPage.php
@@ -116,9 +116,10 @@ class LogPage {
 	 * @static
 	 */
 	public static function logName( $type ) {
-		global $wgLogNames;
+		global $wgLogNames, $wgMessageCache;
 
 		if( isset( $wgLogNames[$type] ) ) {
+			$wgMessageCache->loadAllMessages();
 			return str_replace( '_', ' ', wfMsg( $wgLogNames[$type] ) );
 		} else {
 			// Bogus log types? Perhaps an extension was removed.
@@ -138,7 +139,7 @@ class LogPage {
 	/**
 	 * @static
 	 */
-	static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false, $translate=false ) {
+	static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks=false ) {
 		global $wgLang, $wgContLang, $wgLogActions;
 
 		$key = "$type/$action";
@@ -172,6 +173,11 @@ class LogPage {
 							$text = $wgContLang->ucfirst( $title->getText() );
 							$titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
 							break;
+						case 'merge':
+							$titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
+							$params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
+							$params[1] = $wgLang->timeanddate( $params[1] );
+							break;
 						default:
 							$titleLink = $skin->makeLinkObj( $title );
 					}
@@ -199,8 +205,10 @@ class LogPage {
 				} else {
 					array_unshift( $params, $titleLink );
 					if ( $key == 'block/block' ) {
-						if ( $translate ) {
-							$params[1] = $wgLang->translateBlockExpiry( $params[1] );
+						if ( $skin ) {
+							$params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>';
+						} else {
+							$params[1] = $wgContLang->translateBlockExpiry( $params[1] );
 						}
 						$params[2] = isset( $params[2] )
 										? self::formatBlockFlags( $params[2] )
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index f7a9400d..18c931c5 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -101,6 +101,44 @@ class MagicWord {
 		'numberofadmins',
 		'defaultsort',
 	);
+	
+	/* Array of caching hints for ParserCache */
+	static public $mCacheTTLs = array (
+		'currentmonth' => 86400,
+		'currentmonthname' => 86400,
+		'currentmonthnamegen' => 86400,
+		'currentmonthabbrev' => 86400,
+		'currentday' => 3600,
+		'currentday2' => 3600,
+		'currentdayname' => 3600,
+		'currentyear' => 86400,
+		'currenttime' => 3600,
+		'currenthour' => 3600,
+		'localmonth' => 86400,
+		'localmonthname' => 86400,
+		'localmonthnamegen' => 86400,
+		'localmonthabbrev' => 86400,
+		'localday' => 3600,
+		'localday2' => 3600,
+		'localdayname' => 3600,
+		'localyear' => 86400,
+		'localtime' => 3600,
+		'localhour' => 3600,
+		'numberofarticles' => 3600,
+		'numberoffiles' => 3600,
+		'numberofedits' => 3600,
+		'currentweek' => 3600,
+		'currentdow' => 3600,
+		'localweek' => 3600,
+		'localdow' => 3600,
+		'numberofusers' => 3600,
+		'numberofpages' => 3600,
+		'currentversion' => 86400,
+		'currenttimestamp' => 3600,
+		'localtimestamp' => 3600,
+		'pagesinnamespace' => 3600,
+		'numberofadmins' => 3600,
+		);
 
 	static public $mObjects = array();
 
@@ -122,11 +160,13 @@ class MagicWord {
 	 * @static
 	 */
 	static function &get( $id ) {
+		wfProfileIn( __METHOD__ );
 		if (!array_key_exists( $id, self::$mObjects ) ) {
 			$mw = new MagicWord();
 			$mw->load( $id );
 			self::$mObjects[$id] = $mw;
 		}
+		wfProfileOut( __METHOD__ );
 		return self::$mObjects[$id];
 	}
 
@@ -148,7 +188,17 @@ class MagicWord {
 		}
 		return self::$mVariableIDs;
 	}
-
+	
+	/* Allow external reads of TTL array */
+	static function getCacheTTL($id) {
+		if (array_key_exists($id,self::$mCacheTTLs)) {
+			return self::$mCacheTTLs[$id];
+		} else {
+			return -1;
+		}
+	}
+	
+	
 	# Initialises this object with an ID
 	function load( $id ) {
 		global $wgContLang;
diff --git a/includes/Math.php b/includes/Math.php
index 2771d04c..cfed9554 100644
--- a/includes/Math.php
+++ b/includes/Math.php
@@ -111,10 +111,17 @@ class MathRenderer {
 			} else {
 				$errbit = htmlspecialchars( substr($contents, 1) );
 				switch( $retval ) {
-					case 'E': $errmsg = $this->_error( 'math_lexing_error', $errbit );
-					case 'S': $errmsg = $this->_error( 'math_syntax_error', $errbit );
-					case 'F': $errmsg = $this->_error( 'math_unknown_function', $errbit );
-					default:  $errmsg = $this->_error( 'math_unknown_error', $errbit );
+					case 'E':
+						$errmsg = $this->_error( 'math_lexing_error', $errbit );
+						break;
+					case 'S':
+						$errmsg = $this->_error( 'math_syntax_error', $errbit );
+						break;
+					case 'F':
+						$errmsg = $this->_error( 'math_unknown_function', $errbit );
+						break;
+					default:
+						$errmsg = $this->_error( 'math_unknown_error', $errbit );
 				}
 			}
 
diff --git a/includes/MessageCache.php b/includes/MessageCache.php
index 10c95a7e..ce717fa8 100644
--- a/includes/MessageCache.php
+++ b/includes/MessageCache.php
@@ -58,9 +58,8 @@ class MessageCache {
 	 * Try to load the cache from a local file
 	 */
 	function loadFromLocal( $hash ) {
-		global $wgLocalMessageCache;
+		global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
 
-		$this->mCache = false;
 		if ( $wgLocalMessageCache === false ) {
 			return;
 		}
@@ -74,21 +73,35 @@ class MessageCache {
 			return;
 		}
 
-		// Check to see if the file has the hash specified
-		$localHash = fread( $file, 32 );
-		if ( $hash == $localHash ) {
-			// All good, get the rest of it
-			$serialized = fread( $file, 10000000 );
-			$this->setCache( unserialize( $serialized ) );
+		if ( $wgLocalMessageCacheSerialized ) {
+			// Check to see if the file has the hash specified
+			$localHash = fread( $file, 32 );
+			if ( $hash === $localHash ) {
+				// All good, get the rest of it
+				$serialized = '';
+				while ( !feof( $file ) ) {
+					$serialized .= fread( $file, 100000 );
+				}
+				$this->setCache( unserialize( $serialized ) );
+			}
+			fclose( $file );
+		} else {
+			$localHash=substr(fread($file,40),8);
+			fclose($file);
+			if ($hash!=$localHash) {
+				return;
+			}
+
+			require("$wgLocalMessageCache/messages-" . wfWikiID());
+			$this->setCache( $this->mCache);
 		}
-		fclose( $file );
 	}
 
 	/**
 	 * Save the cache to a local file
 	 */
 	function saveToLocal( $serialized, $hash ) {
-		global $wgLocalMessageCache;
+		global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
 
 		if ( $wgLocalMessageCache === false ) {
 			return;
@@ -111,26 +124,8 @@ class MessageCache {
 	}
 
 	function loadFromScript( $hash ) {
-		global $wgLocalMessageCache;
-		if ( $wgLocalMessageCache === false ) {
-			return;
-		}
-		
-		$filename = "$wgLocalMessageCache/messages-" . wfWikiID();
-		
-		wfSuppressWarnings();
-		$file = fopen( $filename, 'r' );
-		wfRestoreWarnings();
-		if ( !$file ) {
-			return;
-		}
-		$localHash=substr(fread($file,40),8);
-		fclose($file);
-		if ($hash!=$localHash) {
-			return;
-		}
-		require("$wgLocalMessageCache/messages-" . wfWikiID());
-		$this->setCache( $this->mCache);
+		trigger_error( 'Use of ' . __METHOD__ . ' is deprecated', E_USER_NOTICE );
+		$this->loadFromLocal( $hash );
 	}
 	
 	function saveToScript($array, $hash) {
@@ -201,19 +196,17 @@ class MessageCache {
 		$this->mCache = false;
 
 		# Try local cache
-		wfProfileIn( $fname.'-fromlocal' );
-		$hash = $this->mMemc->get( "{$this->mMemcKey}-hash" );
-		if ( $hash ) {
-			if ($wgLocalMessageCacheSerialized) {
+		if ( $wgLocalMessageCache !== false ) {
+			wfProfileIn( $fname.'-fromlocal' );
+			$hash = $this->mMemc->get( "{$this->mMemcKey}-hash" );
+			if ( $hash ) {
 				$this->loadFromLocal( $hash );
-			} else {
-				$this->loadFromScript( $hash );
-			}
-			if ( $this->mCache ) {
-				wfDebug( "MessageCache::load(): got from local cache\n" );
+				if ( $this->mCache ) {
+					wfDebug( "MessageCache::load(): got from local cache\n" );
+				}
 			}
+			wfProfileOut( $fname.'-fromlocal' );
 		}
-		wfProfileOut( $fname.'-fromlocal' );
 
 		# Try memcached
 		if ( !$this->mCache ) {
@@ -358,7 +351,6 @@ class MessageCache {
 		wfProfileIn( __METHOD__ );
 		$this->lock();
 		$this->load();
-		$parserMemc->delete(wfMemcKey('sidebar'));
 		if ( is_array( $this->mCache ) ) {
 			if ( $text === false ) {
 				# Article was deleted
@@ -386,6 +378,7 @@ class MessageCache {
 			}
 		}
 		$this->unlock();
+		$parserMemc->delete(wfMemcKey('sidebar'));
 		wfProfileOut( __METHOD__ );
 	}
 
@@ -475,17 +468,20 @@ class MessageCache {
 		}
 
 		# Try the array of another language
-		if( $message === false && strpos( $lckey, '/' ) ) {
-			$message = explode( '/', $lckey );
-			if ( $message[1] ) {
-				wfSuppressWarnings();
-				$message = Language::getMessageFor( $message[0], $message[1] );
-				wfRestoreWarnings();
-				if ( is_null( $message ) ) {
-					$message = false;
+		$pos = strrpos( $lckey, '/' );
+		if( $message === false && $pos !== false) {
+			$mkey = substr( $lckey, 0, $pos );
+			$code = substr( $lckey, $pos+1 );
+			if ( $code ) {
+				$validCodes = array_keys( Language::getLanguageNames() );
+				if ( in_array( $code, $validCodes ) ) {
+					$message = Language::getMessageFor( $mkey, $code );
+					if ( is_null( $message ) ) {
+						$message = false;
+					}
+				} else {
+					wfDebug( __METHOD__ . ": Invalid code $code for $mkey/$code, not trying messages array\n" );
 				}
-			} else {
-				$message = false;
 			}
 		}
 
@@ -500,9 +496,6 @@ class MessageCache {
 		if( $message === false ) {
 			return '<' . htmlspecialchars($key) . '>';
 		}
-
-		# Replace brace tags
-		$message = $this->transform( $message );
 		return $message;
 	}
 
@@ -576,7 +569,7 @@ class MessageCache {
 		return $message;
 	}
 
-	function transform( $message ) {
+	function transform( $message, $interface = false ) {
 		global $wgParser;
 		if ( !$this->mParser && isset( $wgParser ) ) {
 			# Do some initialisation so that we don't have to do it twice
@@ -584,9 +577,11 @@ class MessageCache {
 			# Clone it and store it
 			$this->mParser = clone $wgParser;
 		}
-		if ( !$this->mDisableTransform && $this->mParser ) {
+		if ( $this->mParser ) {
 			if( strpos( $message, '{{' ) !== false ) {
-				$message = $this->mParser->transformMsg( $message, $this->getParserOptions() );
+				$popts = $this->getParserOptions();
+				$popts->setInterfaceMessage( $interface );
+				$message = $this->mParser->transformMsg( $message, $popts );
 			}
 		}
 		return $message;
@@ -594,10 +589,12 @@ class MessageCache {
 
 	function disable() { $this->mDisable = true; }
 	function enable() { $this->mDisable = false; }
-	function disableTransform() { $this->mDisableTransform = true; }
-	function enableTransform() { $this->mDisableTransform = false; }
-	function setTransform( $x ) { $this->mDisableTransform = $x; }
-	function getTransform() { return $this->mDisableTransform; }
+
+	/** @deprecated */
+	function disableTransform() {}
+	function enableTransform() {}
+	function setTransform( $x ) {}
+	function getTransform() { return false; }
 
 	/**
 	 * Add a message to the cache
@@ -618,6 +615,9 @@ class MessageCache {
 	 */
 	function addMessages( $messages, $lang = 'en' ) {
 		wfProfileIn( __METHOD__ );
+		if ( !is_array( $messages ) ) {
+			throw new MWException( __METHOD__.': Invalid message array' );
+		}
 		if ( isset( $this->mExtensionMessages[$lang] ) ) {
 			$this->mExtensionMessages[$lang] = $messages + $this->mExtensionMessages[$lang];
 		} else {
@@ -640,7 +640,8 @@ class MessageCache {
 	}
 
 	/**
-	 * Get the extension messages for a specific language
+	 * Get the extension messages for a specific language. Only English, interface
+	 * and content language are guaranteed to be loaded.
 	 *
 	 * @param string $lang The messages language, English by default
 	 */
@@ -695,9 +696,28 @@ class MessageCache {
 	 * Load messages from a given file
 	 */
 	function loadMessagesFile( $filename ) {
-		$magicWords = false;
+		global $wgLang, $wgContLang;
+		$messages = $magicWords = false;
 		require( $filename );
-		$this->addMessagesByLang( $messages );
+
+		/*
+		 * Load only languages that are usually used, and merge all fallbacks,
+		 * except English.
+		 */
+		$langs = array_unique( array( 'en', $wgContLang->getCode(), $wgLang->getCode() ) );
+		foreach( $langs as $code ) {
+			$fbcode = $code;
+			$mergedMessages = array();
+			do {
+				if ( isset($messages[$fbcode]) ) {
+					$mergedMessages += $messages[$fbcode];
+				}
+				$fbcode = Language::getFallbackfor( $fbcode );
+			} while( $fbcode && $fbcode !== 'en' );
+
+			if ( !empty($mergedMessages) )
+				$this->addMessages( $mergedMessages, $code );
+		}
 
 		if ( $magicWords !== false ) {
 			global $wgContLang;
@@ -705,4 +725,3 @@ class MessageCache {
 		}
 	}
 }
-
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index 264a3595..2ca5892f 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -24,8 +24,9 @@ image/jpeg jpeg jpg jpe
 image/png png
 image/svg+xml image/svg svg
 image/tiff tiff tif
-image/vnd.djvu djvu
+image/vnd.djvu image/x.djvu image/x-djvu djvu
 image/x-portable-pixmap ppm
+image/x-xcf xcf
 text/plain txt
 text/html html htm
 video/ogg ogm ogg
@@ -54,6 +55,7 @@ image/png [BITMAP]
 image/svg+xml [DRAWING]
 image/tiff [BITMAP]
 image/vnd.djvu [BITMAP]
+image/x-xcf [BITMAP]
 image/x-portable-pixmap [BITMAP]
 text/plain [TEXT]
 text/html [TEXT]
@@ -351,10 +353,17 @@ class MimeMagic {
 	 */
 	function isRecognizableExtension( $extension ) {
 		static $types = array(
+			// Types recognized by getimagesize()
 			'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd',
 			'bmp', 'tiff', 'tif', 'jpc', 'jp2',
 			'jpx', 'jb2', 'swc', 'iff', 'wbmp',
-			'xbm', 'djvu'
+			'xbm',
+			
+			// Formats we recognize magic numbers for
+			'djvu', 'ogg', 'mid', 'pdf', 'wmf', 'xcf',
+			
+			// XML formats we sure hope we recognize reliably
+			'svg',
 		);
 		return in_array( strtolower( $extension ), $types );
 	}
@@ -371,8 +380,22 @@ class MimeMagic {
 	* @return string the mime type of $file
 	*/
 	function guessMimeType( $file, $ext = true ) {
-		$mime = $this->detectMimeType( $file, $ext );
+		$mime = $this->doGuessMimeType( $file, $ext );
+
+		if( !$mime ) {
+			wfDebug( __METHOD__.": internal type detection failed for $file (.$ext)...\n" );
+			$mime = $this->detectMimeType( $file, $ext );
+		}
+
+		if ( isset( $this->mMimeTypeAliases[$mime] ) ) {
+			$mime = $this->mMimeTypeAliases[$mime];
+		}
 
+		wfDebug(__METHOD__.": final mime type of $file: $mime\n");
+		return $mime;
+	}
+	
+	function doGuessMimeType( $file, $ext = true ) {
 		// Read a chunk of the file
 		wfSuppressWarnings();
 		$f = fopen( $file, "rt" );
@@ -381,128 +404,132 @@ class MimeMagic {
 		$head = fread( $f, 1024 );
 		fclose( $f );
 
-		$sub4 =  substr( $head, 0, 4 );
-		if ( $sub4 == "\x01\x00\x09\x00" || $sub4 == "\xd7\xcd\xc6\x9a" ) {
-			// WMF kill kill kill
+		// Hardcode a few magic number checks...
+		$headers = array(
+			// Multimedia...
+			'MThd'             => 'audio/midi',
+			'OggS'             => 'application/ogg',
+			
+			// Image formats...
 			// Note that WMF may have a bare header, no magic number.
-			// The former of the above two checks is theoretically prone to false positives
-			$mime = "application/x-msmetafile";
+			"\x01\x00\x09\x00" => 'application/x-msmetafile', // Possibly prone to false positives?
+			"\xd7\xcd\xc6\x9a" => 'application/x-msmetafile',
+			'%PDF'             => 'application/pdf',
+			'gimp xcf'         => 'image/x-xcf',
+			
+			// Some forbidden fruit...
+			'MZ'               => 'application/octet-stream', // DOS/Windows executable
+			"\xca\xfe\xba\xbe" => 'application/octet-stream', // Mach-O binary
+			"\x7fELF"          => 'application/octet-stream', // ELF binary
+		);
+		
+		foreach( $headers as $magic => $candidate ) {
+			if( strncmp( $head, $magic, strlen( $magic ) ) == 0 ) {
+				wfDebug( __METHOD__ . ": magic header in $file recognized as $candidate\n" );
+				return $candidate;
+			}
 		}
 
-		if ( strpos( $mime, "text/" ) === 0 || $mime === "application/xml" ) {
-
-			$xml_type = NULL;
-			$script_type = NULL;
-
-			/*
-			* look for XML formats (XHTML and SVG)
-			*/
-			if ($mime === "text/sgml" ||
-			    $mime === "text/plain" ||
-			    $mime === "text/html" ||
-			    $mime === "text/xml" ||
-			    $mime === "application/xml") {
-
-				if ( substr( $head, 0, 5 ) == "<?xml" ) {
-					$xml_type = "ASCII";
-				} elseif ( substr( $head, 0, 8 ) == "\xef\xbb\xbf<?xml") {
-					$xml_type = "UTF-8";
-				} elseif ( substr( $head, 0, 10 ) == "\xfe\xff\x00<\x00?\x00x\x00m\x00l" ) {
-					$xml_type = "UTF-16BE";
-				} elseif ( substr( $head, 0, 10 ) == "\xff\xfe<\x00?\x00x\x00m\x00l\x00") {
-					$xml_type = "UTF-16LE";
-				}
-
-				if ( $xml_type ) {
-					if ( $xml_type !== "UTF-8" && $xml_type !== "ASCII" ) {
-						$head = iconv( $xml_type, "ASCII//IGNORE", $head );
-					}
-
-					$match = array();
-					$doctype = "";
-					$tag = "";
-
-					if ( preg_match( '%<!DOCTYPE\s+[\w-]+\s+PUBLIC\s+["'."'".'"](.*?)["'."'".'"].*>%sim', 
-						$head, $match ) ) {
-							$doctype = $match[1];
-						}
-					if ( preg_match( '%<(\w+).*>%sim', $head, $match ) ) {
-						$tag = $match[1];
-					}
-
-					#print "<br>ANALYSING $file ($mime): doctype= $doctype; tag= $tag<br>";
-
-					if ( strpos( $doctype, "-//W3C//DTD SVG" ) === 0 ) {
-						$mime = "image/svg+xml";
-					} elseif ( $tag === "svg" ) {
-						$mime = "image/svg+xml";
-					} elseif ( strpos( $doctype, "-//W3C//DTD XHTML" ) === 0 ) {
-						$mime = "text/html";
-					} elseif ( $tag === "html" ) {
-						$mime = "text/html";
-					}
-				}
+		/*
+		 * look for PHP
+		 * Check for this before HTML/XML...
+		 * Warning: this is a heuristic, and won't match a file with a lot of non-PHP before.
+		 * It will also match text files which could be PHP. :)
+		 */
+		if( ( strpos( $head, '<?php' ) !== false ) ||
+		    ( strpos( $head, '<? ' ) !== false ) ||
+		    ( strpos( $head, "<?\n" ) !== false ) ||
+		    ( strpos( $head, "<?\t" ) !== false ) ||
+		    ( strpos( $head, "<?=" ) !== false ) ||
+
+		    ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) ||
+		    ( strpos( $head, "<\x00?\x00 " ) !== false ) ||
+		    ( strpos( $head, "<\x00?\x00\n" ) !== false ) ||
+		    ( strpos( $head, "<\x00?\x00\t" ) !== false ) ||
+		    ( strpos( $head, "<\x00?\x00=" ) !== false ) ) {
+
+			wfDebug( __METHOD__ . ": recognized $file as application/x-php\n" );
+			return "application/x-php";
+		}
+		
+		/*
+		 * look for XML formats (XHTML and SVG)
+		 */
+		$xml = new XmlTypeCheck( $file );
+		if( $xml->wellFormed ) {
+			$types = array(
+				'http://www.w3.org/2000/svg:svg'    => 'image/svg+xml',
+				'svg'                               => 'image/svg+xml',
+				'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml?
+				'html'                              => 'text/html', // application/xhtml+xml?
+			);
+			if( isset( $types[$xml->rootElement] ) ) {
+				$mime = $types[$xml->rootElement];
+				return $mime;
+			} else {
+				/// Fixme -- this would be the place to allow additional XML type checks
+				return 'application/xml';
 			}
+		}
 
-			/*
-			* look for shell scripts
-			*/
-			if ( !$xml_type ) {
-				$script_type = NULL;
-
-				# detect by shebang
-				if ( substr( $head, 0, 2) == "#!" ) {
-					$script_type = "ASCII";
-				} elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) {
-					$script_type = "UTF-8";
-				} elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) {
-					$script_type = "UTF-16BE";
-				} elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) {
-					$script_type= "UTF-16LE";
-				}
-
-				if ( $script_type ) {
-					if ( $script_type !== "UTF-8" && $script_type !== "ASCII") {
-						$head = iconv( $script_type, "ASCII//IGNORE", $head);
-					}
-
-					$match = array();
+		/*
+		 * look for shell scripts
+		 */
+		$script_type = NULL;
+
+		# detect by shebang
+		if ( substr( $head, 0, 2) == "#!" ) {
+			$script_type = "ASCII";
+		} elseif ( substr( $head, 0, 5) == "\xef\xbb\xbf#!" ) {
+			$script_type = "UTF-8";
+		} elseif ( substr( $head, 0, 7) == "\xfe\xff\x00#\x00!" ) {
+			$script_type = "UTF-16BE";
+		} elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) {
+			$script_type= "UTF-16LE";
+		}
 
-					if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) {
-						$mime = "application/x-{$match[2]}";
+		if ( $script_type ) {
+			if ( $script_type !== "UTF-8" && $script_type !== "ASCII") {
+				// Quick and dirty fold down to ASCII!
+				$pack = array( 'UTF-16BE' => 'n*', 'UTF-16LE' => 'v*' );
+				$chars = unpack( $pack[$script_type], substr( $head, 2 ) );
+				$head = '';
+				foreach( $chars as $codepoint ) {
+					if( $codepoint < 128 ) {
+						$head .= chr( $codepoint );
+					} else {
+						$head .= '?';
 					}
 				}
 			}
 
-			/*
-			* look for PHP
-			*/
-			if( !$xml_type && !$script_type ) {
-
-				if( ( strpos( $head, '<?php' ) !== false ) ||
-				    ( strpos( $head, '<? ' ) !== false ) ||
-				    ( strpos( $head, "<?\n" ) !== false ) ||
-				    ( strpos( $head, "<?\t" ) !== false ) ||
-				    ( strpos( $head, "<?=" ) !== false ) ||
-
-				    ( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) ||
-				    ( strpos( $head, "<\x00?\x00 " ) !== false ) ||
-				    ( strpos( $head, "<\x00?\x00\n" ) !== false ) ||
-				    ( strpos( $head, "<\x00?\x00\t" ) !== false ) ||
-				    ( strpos( $head, "<\x00?\x00=" ) !== false ) ) {
+			$match = array();
 
-					$mime = "application/x-php";
-				}
+			if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) {
+				$mime = "application/x-{$match[2]}";
+				wfDebug( __METHOD__.": shell script recognized as $mime\n" );
+				return $mime;
 			}
-
 		}
-
-		if ( isset( $this->mMimeTypeAliases[$mime] ) ) {
-			$mime = $this->mMimeTypeAliases[$mime];
+		
+		wfSuppressWarnings();
+		$gis = getimagesize( $file );
+		wfRestoreWarnings();
+		
+		if( $gis && isset( $gis['mime'] ) ) {
+			$mime = $gis['mime'];
+			wfDebug( __METHOD__.": getimagesize detected $file as $mime\n" );
+			return $mime;
+		} else {
+			return false;
 		}
 
-		wfDebug(__METHOD__.": final mime type of $file: $mime\n");
-		return $mime;
+		// Also test DjVu
+		$deja = new DjVuImage( $file );
+		if( $deja->isValid() ) {
+			wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" );
+			return 'image/vnd.djvu';
+		}
 	}
 
 	/** Internal mime type detection, please use guessMimeType() for application code instead.
@@ -559,15 +586,6 @@ class MimeMagic {
 			# see http://www.php.net/manual/en/ref.mime-magic.php for details.
 
 			$m = mime_content_type($file);
-
-			if ( $m == 'text/plain' ) {
-				// mime_content_type sometimes considers DJVU files to be text/plain.
-				$deja = new DjVuImage( $file );
-				if( $deja->isValid() ) {
-					wfDebug( __METHOD__.": (re)detected $file as image/vnd.djvu\n" );
-					$m = 'image/vnd.djvu';
-				}
-			}
 		} else {
 			wfDebug( __METHOD__.": no magic mime detector found!\n" );
 		}
@@ -586,66 +604,20 @@ class MimeMagic {
 			}
 		}
 
-		# if still not known, use getimagesize to find out the type of image
-		# TODO: skip things that do not have a well-known image extension? Would that be safe?
-		wfSuppressWarnings();
-		$gis = getimagesize( $file );
-		wfRestoreWarnings();
-
-		$notAnImage = false;
-
-		if ( $gis && is_array($gis) && $gis[2] ) {
-			
-			switch ( $gis[2] ) {
-				case IMAGETYPE_GIF: $m = "image/gif"; break;
-				case IMAGETYPE_JPEG: $m = "image/jpeg"; break;
-				case IMAGETYPE_PNG: $m = "image/png"; break;
-				case IMAGETYPE_SWF: $m = "application/x-shockwave-flash"; break;
-				case IMAGETYPE_PSD: $m = "application/photoshop"; break;
-				case IMAGETYPE_BMP: $m = "image/bmp"; break;
-				case IMAGETYPE_TIFF_II: $m = "image/tiff"; break;
-				case IMAGETYPE_TIFF_MM: $m = "image/tiff"; break;
-				case IMAGETYPE_JPC: $m = "image"; break;
-				case IMAGETYPE_JP2: $m = "image/jpeg2000"; break;
-				case IMAGETYPE_JPX: $m = "image/jpeg2000"; break;
-				case IMAGETYPE_JB2: $m = "image"; break;
-				case IMAGETYPE_SWC: $m = "application/x-shockwave-flash"; break;
-				case IMAGETYPE_IFF: $m = "image/vnd.xiff"; break;
-				case IMAGETYPE_WBMP: $m = "image/vnd.wap.wbmp"; break;
-				case IMAGETYPE_XBM: $m = "image/x-xbitmap"; break;
-			}
-
-			if ( $m ) {
-				wfDebug( __METHOD__.": image mime type of $file: $m\n" );
-				return $m;
-			}
-			else {
-				$notAnImage = true;
-			}
-		} else {
-			// Also test DjVu
-			$deja = new DjVuImage( $file );
-			if( $deja->isValid() ) {
-				wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" );
-				return 'image/vnd.djvu';
-			}
-		}
-
 		# if desired, look at extension as a fallback.
 		if ( $ext === true ) {
 			$i = strrpos( $file, '.' );
 			$ext = strtolower( $i ? substr( $file, $i + 1 ) : '' );
 		}
 		if ( $ext ) {
-			$m = $this->guessTypesForExtension( $ext );
-
-			# TODO: if $notAnImage is set, do not trust the file extension if
-			# the results is one of the image types that should have been recognized
-			# by getimagesize
-
-			if ( $m ) {
-				wfDebug( __METHOD__.": extension mime type of $file: $m\n" );
-				return $m;
+			if( $this->isRecognizableExtension( $ext ) ) {
+				wfDebug( __METHOD__. ": refusing to guess mime type for .$ext file, we should have recognized it\n" );
+			} else {
+				$m = $this->guessTypesForExtension( $ext );
+				if ( $m ) {
+					wfDebug( __METHOD__.": extension mime type of $file: $m\n" );
+					return $m;
+				}
 			}
 		}
 
diff --git a/includes/Namespace.php b/includes/Namespace.php
index f4df3bac..57a71282 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -41,6 +41,11 @@ if( is_array( $wgExtraNamespaces ) ) {
  * Users and translators should not change them
  *
  */
+
+/*
+WARNING: The statement below may fail on some versions of PHP: see bug 12294
+*/
+
 class Namespace {
 
 	/**
diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php
index d8ac12b5..107553fc 100644
--- a/includes/OutputHandler.php
+++ b/includes/OutputHandler.php
@@ -4,8 +4,21 @@
  * Standard output handler for use with ob_start
  */
 function wfOutputHandler( $s ) {
-	global $wgDisableOutputCompression;
-	$s = wfMangleFlashPolicy( $s );
+	global $wgDisableOutputCompression, $wgValidateAllHtml;
+    $s = wfMangleFlashPolicy( $s );
+    if ( $wgValidateAllHtml ) {
+		$headers = apache_response_headers();
+		$isHTML = true;
+		foreach ( $headers as $name => $value ) {
+			if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false ) {
+				$isHTML = false;
+				break;
+			}
+		}
+		if ( $isHTML ) {
+			$s = wfHtmlValidationHandler( $s );
+		}
+	}
 	if ( !$wgDisableOutputCompression && !ini_get( 'zlib.output_compression' ) ) {
 		if ( !defined( 'MW_NO_OUTPUT_COMPRESSION' ) ) {
 			$s = wfGzipHandler( $s );
@@ -61,10 +74,12 @@ function wfGzipHandler( $s ) {
 		return $s;
 	}
 	
-	$tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] );
-	if ( in_array( 'gzip', $tokens ) ) {
-		header( 'Content-Encoding: gzip' );
-		$s = gzencode( $s, 3 );
+	if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
+		$tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] );
+		if ( in_array( 'gzip', $tokens ) ) {
+			header( 'Content-Encoding: gzip' );
+			$s = gzencode( $s, 3 );
+		}
 	}
 	
 	// Set vary header if it hasn't been set already
@@ -78,6 +93,7 @@ function wfGzipHandler( $s ) {
 	}
 	if ( !$foundVary ) {
 		header( 'Vary: Accept-Encoding' );
+		header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip' );
 	}
 	return $s;
 }
@@ -98,4 +114,60 @@ function wfDoContentLength( $length ) {
 	}
 }
 
+/**
+ * Replace the output with an error if the HTML is not valid
+ */
+function wfHtmlValidationHandler( $s ) {
+	global $IP;
+	$tidy = new tidy;
+	$tidy->parseString( $s, "$IP/includes/tidy.conf", 'utf8' );
+	if ( $tidy->getStatus() == 0 ) {
+		return $s;
+	}
+
+	header( 'Cache-Control: no-cache' );
+
+	$out = <<<EOT
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+<title>HTML validation error
+
+
+
+

HTML validation error

+
    +EOT; + + $error = strtok( $tidy->errorBuffer, "\n" ); + $badLines = array(); + while ( $error !== false ) { + if ( preg_match( '/^line (\d+)/', $error, $m ) ) { + $lineNum = intval( $m[1] ); + $badLines[$lineNum] = true; + $out .= "
  • " . htmlspecialchars( $error ) . "
  • \n"; + } + $error = strtok( "\n" ); + } + + $out .= '
    ' . htmlspecialchars( $tidy->errorBuffer ) . '
    '; + $out .= '
      '; + $line = strtok( $s, "\n" ); + $i = 1; + while ( $line !== false ) { + if ( isset( $badLines[$i] ) ) { + $out .= "
    1. "; + } else { + $out .= '
    2. '; + } + $out .= htmlspecialchars( $line ) . '
    3. '; + $line = strtok( "\n" ); + $i++; + } + $out .= '
    '; + return $out; +} diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 06467157..1fddeb7d 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -23,12 +23,14 @@ class OutputPage { var $mIsArticleRelated; protected $mParserOptions; // lazy initialised, use parserOptions() var $mShowFeedLinks = false; + var $mFeedLinksAppendQuery = false; var $mEnableClientCache = true; var $mArticleBodyOnly = false; var $mNewSectionLink = false; var $mNoGallery = false; var $mPageTitleActionText = ''; + var $mParseWarnings = array(); /** * Constructor @@ -63,6 +65,10 @@ class OutputPage { $this->mRedirect = str_replace( "\n", '', $url ); $this->mRedirectCode = $responsecode; } + + public function getRedirect() { + return $this->mRedirect; + } /** * Set the HTTP status code to send with the output. @@ -228,6 +234,8 @@ class OutputPage { public function isPrintable() { return $this->mPrintable; } public function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; } public function isSyndicated() { return $this->mShowFeedLinks; } + public function setFeedAppendQuery( $val ) { $this->mFeedLinksAppendQuery = $val; } + public function getFeedAppendQuery() { return $this->mFeedLinksAppendQuery; } public function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } public function getOnloadHandler() { return $this->mOnloadHandler; } public function disable() { $this->mDoNothing = true; } @@ -351,10 +359,12 @@ class OutputPage { wfIncrStats('pcache_not_possible'); $popts = $this->parserOptions(); - $popts->setTidy($tidy); + $oldTidy = $popts->setTidy($tidy); $parserOutput = $wgParser->parse( $text, $title, $popts, $linestart, true, $this->mRevisionId ); + + $popts->setTidy( $oldTidy ); $this->addParserOutput( $parserOutput ); @@ -370,6 +380,7 @@ class OutputPage { $this->addCategoryLinks( $parserOutput->getCategories() ); $this->mNewSectionLink = $parserOutput->getNewSection(); $this->addKeywords( $parserOutput ); + $this->mParseWarnings = $parserOutput->getWarnings(); if ( $parserOutput->getCacheTime() == -1 ) { $this->enableClientCache( false ); } @@ -514,16 +525,33 @@ class OutputPage { && $wgRequest->getText('uselang', false) === false; } + /** Get a complete X-Vary-Options header */ + public function getXVO() { + global $wgCookiePrefix; + return 'X-Vary-Options: ' . + # User ID cookie + "Cookie;string-contains={$wgCookiePrefix}UserID;" . + # Session cookie + 'string-contains=' . session_name() . ',' . + # Encoding checks for gzip only + 'Accept-Encoding;list-contains=gzip'; + } + public function sendCacheControl() { global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest; $fname = 'OutputPage::sendCacheControl'; + $response = $wgRequest->response(); if ($wgUseETag && $this->mETag) - $wgRequest->response()->header("ETag: $this->mETag"); + $response->header("ETag: $this->mETag"); # don't serve compressed data to clients who can't handle it # maintain different caches for logged-in users and non-logged in ones - $wgRequest->response()->header( 'Vary: Accept-Encoding, Cookie' ); + $response->header( 'Vary: Accept-Encoding, Cookie' ); + + # Add an X-Vary-Options header for Squid with Wikimedia patches + $response->header( $this->getXVO() ); + if( !$this->uncacheableBecauseRequestvars() && $this->mEnableClientCache ) { if( $wgUseSquid && session_id() == '' && ! $this->isPrintable() && $this->mSquidMaxage != 0 ) @@ -535,8 +563,8 @@ class OutputPage { wfDebug( "$fname: proxy caching with ESI; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( 'Surrogate-Control: max-age=2678400+2678400, content="ESI/1.0"'); - $wgRequest->response()->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); - $wgRequest->response()->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); + $response->header( 'Surrogate-Control: max-age='.$wgSquidMaxage.'+'.$this->mSquidMaxage.', content="ESI/1.0"'); + $response->header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' ); } else { # We'll purge the proxy cache for anons explicitly, but require end user agents # to revalidate against the proxy on each visit. @@ -545,24 +573,24 @@ class OutputPage { wfDebug( "$fname: local proxy caching; {$this->mLastModified} **\n", false ); # start with a shorter timeout for initial testing # header( "Cache-Control: s-maxage=2678400, must-revalidate, max-age=0" ); - $wgRequest->response()->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); + $response->header( 'Cache-Control: s-maxage='.$this->mSquidMaxage.', must-revalidate, max-age=0' ); } } else { # We do want clients to cache if they can, but they *must* check for updates # on revisiting the page. wfDebug( "$fname: private caching; {$this->mLastModified} **\n", false ); - $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); - $wgRequest->response()->header( "Cache-Control: private, must-revalidate, max-age=0" ); + $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $response->header( "Cache-Control: private, must-revalidate, max-age=0" ); } - if($this->mLastModified) $wgRequest->response()->header( "Last-modified: {$this->mLastModified}" ); + if($this->mLastModified) $response->header( "Last-modified: {$this->mLastModified}" ); } else { wfDebug( "$fname: no caching **\n", false ); # In general, the absence of a last modified header should be enough to prevent # the client from using its cache. We send a few other things just to make sure. - $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' ); + $response->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $response->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $response->header( 'Pragma: no-cache' ); } } @@ -581,29 +609,10 @@ class OutputPage { } $fname = 'OutputPage::output'; wfProfileIn( $fname ); - $sk = $wgUser->getSkin(); - - if ( $wgUseAjax ) { - $this->addScript( "\n" ); - - wfRunHooks( 'AjaxAddScript', array( &$this ) ); - - if( $wgAjaxSearch ) { - $this->addScript( "\n" ); - $this->addScript( "\n" ); - } - - if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { - $this->addScript( "\n" ); - } - } if ( '' != $this->mRedirect ) { - if( substr( $this->mRedirect, 0, 4 ) != 'http' ) { - # Standards require redirect URLs to be absolute - global $wgServer; - $this->mRedirect = $wgServer . $this->mRedirect; - } + # Standards require redirect URLs to be absolute + $this->mRedirect = wfExpandUrl( $this->mRedirect ); if( $this->mRedirectCode == '301') { if( !$wgDebugRedirects ) { $wgRequest->response()->header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently"); @@ -680,6 +689,25 @@ class OutputPage { $wgRequest->response()->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] ); } + $sk = $wgUser->getSkin(); + + if ( $wgUseAjax ) { + $this->addScript( "\n" ); + + wfRunHooks( 'AjaxAddScript', array( &$this ) ); + + if( $wgAjaxSearch && $wgUser->getBoolOption( 'ajaxsearch' ) ) { + $this->addScript( "\n" ); + $this->addScript( "\n" ); + } + + if( $wgAjaxWatch && $wgUser->isLoggedIn() ) { + $this->addScript( "\n" ); + } + } + + + # Buffer output; final headers may depend on later processing ob_start(); @@ -758,6 +786,9 @@ class OutputPage { $name = User::whoIs( $wgUser->blockedBy() ); $reason = $wgUser->blockedFor(); + if( $reason == '' ) { + $reason = wfMsg( 'blockednoreason' ); + } $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true ); $ip = wfGetIP(); @@ -793,7 +824,7 @@ class OutputPage { * This could be a username, an ip range, or a single ip. */ $intended = $wgUser->mBlock->mAddress; - $this->addWikiText( wfMsg( $msg, $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ) ); + $this->addWikiMsg( $msg, $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ); # Don't auto-return to special pages if( $return ) { @@ -811,9 +842,9 @@ class OutputPage { */ public function showErrorPage( $title, $msg, $params = array() ) { global $wgTitle; - - $this->mDebugtext .= 'Original title: ' . - $wgTitle->getPrefixedText() . "\n"; + if ( isset($wgTitle) ) { + $this->mDebugtext .= 'Original title: ' . $wgTitle->getPrefixedText() . "\n"; + } $this->setPageTitle( wfMsg( $title ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); $this->setRobotpolicy( 'noindex,nofollow' ); @@ -839,7 +870,7 @@ class OutputPage { global $wgTitle; $this->mDebugtext .= 'Original title: ' . - $wgTitle->getPrefixedText() . "\n"; + $wgTitle->getPrefixedText() . "\n"; $this->setPageTitle( wfMsg( 'permissionserrors' ) ); $this->setHTMLTitle( wfMsg( 'permissionserrors' ) ); $this->setRobotpolicy( 'noindex,nofollow' ); @@ -868,7 +899,7 @@ class OutputPage { $this->setArticleRelated( false ); $this->mBodytext = ''; - $this->addWikiText( wfMsg( 'versionrequiredtext', $version ) ); + $this->addWikiMsg( 'versionrequiredtext', $version ); $this->returnToMain(); } @@ -967,36 +998,46 @@ class OutputPage { /** * @param array $errors An array of arrays returned by Title::getUserPermissionsErrors - * @return string The error-messages, formatted into a list. + * @return string The wikitext error-messages, formatted into a list. */ public function formatPermissionsErrorMessage( $errors ) { - $text = ''; + $text = wfMsgNoTrans( 'permissionserrorstext', count( $errors ) ) . "\n\n"; - if (sizeof( $errors ) > 1) { - - $text .= wfMsgExt( 'permissionserrorstext', array( 'parse' ), count( $errors ) ) . "\n"; + if (count( $errors ) > 1) { $text .= '
      ' . "\n"; foreach( $errors as $error ) { $text .= '
    • '; - $text .= call_user_func_array( 'wfMsg', $error ); + $text .= call_user_func_array( 'wfMsgNoTrans', $error ); $text .= "
    • \n"; } $text .= '
    '; } else { - $text .= call_user_func_array( 'wfMsg', $errors[0]); + $text .= '
    ' . call_user_func_array( 'wfMsgNoTrans', $errors[0]) . '
    '; } return $text; } /** - * @todo document - * @param bool $protected Is the reason the page can't be reached because it's protected? - * @param mixed $source - * @param bool $protected, page is protected? - * @param array $reason, array of arrays( msg, args ) + * Display a page stating that the Wiki is in read-only mode, + * and optionally show the source of the page that the user + * was trying to edit. Should only be called (for this + * purpose) after wfReadOnly() has returned true. + * + * For historical reasons, this function is _also_ used to + * show the error message when a user tries to edit a page + * they are not allowed to edit. (Unless it's because they're + * blocked, then we show blockedPage() instead.) In this + * case, the second parameter should be set to true and a list + * of reasons supplied as the third parameter. + * + * @todo Needs to be split into multiple functions. + * + * @param string $source Source code to show (or null). + * @param bool $protected Is this a permissions error? + * @param array $reasons List of reasons for this error, as returned by Title::getUserPermissionsErrors(). */ public function readOnlyPage( $source = null, $protected = false, $reasons = array() ) { global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle; @@ -1004,61 +1045,59 @@ class OutputPage { $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); - - if ( !empty($reasons) ) { - $this->setPageTitle( wfMsg( 'viewsource' ) ); - $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) ); - } else if( $protected ) { - $this->setPageTitle( wfMsg( 'viewsource' ) ); - $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - list( $cascadeSources, /* $restrictions */ ) = $wgTitle->getCascadeProtectionSources(); - - // Show an appropriate explanation depending upon the reason - // for the protection...all of these should be moved to the - // callers - if( $wgTitle->getNamespace() == NS_MEDIAWIKI ) { - // User isn't allowed to edit the interface - $this->addWikiText( wfMsg( 'protectedinterface' ) ); - } elseif( $cascadeSources && ( $count = count( $cascadeSources ) ) > 0 ) { - // Cascading protection - $titles = ''; - foreach( $cascadeSources as $title ) - $titles .= "* [[:" . $title->getPrefixedText() . "]]\n"; - $this->addWikiText( wfMsgExt( 'cascadeprotected', 'parsemag', $count ) . "\n{$titles}" ); - } elseif( !$wgTitle->isProtected( 'edit' ) && $wgTitle->isNamespaceProtected() ) { - // Namespace protection - $ns = $wgTitle->getNamespace() == NS_MAIN - ? wfMsg( 'nstab-main' ) - : $wgTitle->getNsText(); - $this->addWikiText( wfMsg( 'namespaceprotected', $ns ) ); + // If no reason is given, just supply a default "I can't let you do + // that, Dave" message. Should only occur if called by legacy code. + if ( $protected && empty($reasons) ) { + $reasons[] = array( 'badaccess-group0' ); + } + + if ( !empty($reasons) ) { + // Permissions error + if( $source ) { + $this->setPageTitle( wfMsg( 'viewsource' ) ); + $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); } else { - // Standard protection - $this->addWikiText( wfMsg( 'protectedpagetext' ) ); + $this->setPageTitle( wfMsg( 'badaccess' ) ); } + $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) ); } else { + // Wiki is read only $this->setPageTitle( wfMsg( 'readonly' ) ); if ( $wgReadOnly ) { $reason = $wgReadOnly; } else { + // Should not happen, user should have called wfReadOnly() first $reason = file_get_contents( $wgReadOnlyFile ); } - $this->addWikiText( wfMsg( 'readonlytext', $reason ) ); + $this->addWikiMsg( 'readonlytext', $reason ); } + // Show source, if supplied if( is_string( $source ) ) { - $this->addWikiText( wfMsg( 'viewsourcetext' ) ); - $rows = $wgUser->getIntOption( 'rows' ); - $cols = $wgUser->getIntOption( 'cols' ); - $text = "\n"; + $this->addWikiMsg( 'viewsourcetext' ); + $text = wfOpenElement( 'textarea', + array( 'id' => 'wpTextbox1', + 'name' => 'wpTextbox1', + 'cols' => $wgUser->getOption( 'cols' ), + 'rows' => $wgUser->getOption( 'rows' ), + 'readonly' => 'readonly' ) ); + $text .= htmlspecialchars( $source ); + $text .= wfCloseElement( 'textarea' ); $this->addHTML( $text ); + + // Show templates used by this article + $skin = $wgUser->getSkin(); + $article = new Article( $wgTitle ); + $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); } - $article = new Article( $wgTitle ); - $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); - $this->returnToMain( false ); + # If the title doesn't exist, it's fairly pointless to print a return + # link to it. After all, you just tried editing it and couldn't, so + # what's there to do there? + if( $wgTitle->exists() ) { + $this->returnToMain( false, $wgTitle ); + } } /** @deprecated */ @@ -1275,28 +1314,87 @@ class OutputPage { } $ret .= " />\n"; } - if( $this->isSyndicated() ) { - # FIXME: centralize the mime-type and name information in Feed.php - $link = $wgRequest->escapeAppendQuery( 'feed=rss' ); - $ret .= "\n"; - $link = $wgRequest->escapeAppendQuery( 'feed=atom' ); - $ret .= "\n"; + + foreach( $this->getSyndicationLinks() as $format => $link ) { + # Use the page name for the title (accessed through $wgTitle since + # there's no other way). In principle, this could lead to issues + # with having the same name for different feeds corresponding to + # the same page, but we can't avoid that at this low a level. + global $wgTitle; + + $ret .= $this->feedLink( + $format, + $link, + wfMsg( "page-{$format}-feed", $wgTitle->getPrefixedText() ) ); # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep) } + # Recent changes feed should appear on every page + # Put it after the per-page feed to avoid changing existing behavior. + # It's still available, probably via a menu in your browser. + global $wgSitename; + $rctitle = SpecialPage::getTitleFor( 'Recentchanges' ); + $ret .= $this->feedLink( + 'rss', + $rctitle->getFullURL( 'feed=rss' ), + wfMsg( 'site-rss-feed', $wgSitename ) ); + $ret .= $this->feedLink( + 'atom', + $rctitle->getFullURL( 'feed=atom' ), + wfMsg( 'site-atom-feed', $wgSitename ) ); + return $ret; } + + /** + * Return URLs for each supported syndication format for this page. + * @return array associating format keys with URLs + */ + public function getSyndicationLinks() { + global $wgTitle, $wgFeedClasses; + $links = array(); + + if( $this->isSyndicated() ) { + if( is_string( $this->getFeedAppendQuery() ) ) { + $appendQuery = "&" . $this->getFeedAppendQuery(); + } else { + $appendQuery = ""; + } + + foreach( $wgFeedClasses as $format => $class ) { + $links[$format] = $wgTitle->getLocalUrl( "feed=$format{$appendQuery}" ); + } + } + return $links; + } + + /** + * Generate a for an RSS feed. + */ + private function feedLink( $type, $url, $text ) { + return Xml::element( 'link', array( + 'rel' => 'alternate', + 'type' => "application/$type+xml", + 'title' => $text, + 'href' => $url ) ) . "\n"; + } /** * Turn off regular page output and return an error reponse * for when rate limiting has triggered. - * @todo i18n */ public function rateLimited() { - global $wgOut; - $wgOut->disable(); - wfHttpError( 500, 'Internal Server Error', - 'Sorry, the server has encountered an internal error. ' . - 'Please wait a moment and hit "refresh" to submit the request again.' ); + global $wgOut, $wgTitle; + + $this->setPageTitle(wfMsg('actionthrottled')); + $this->setRobotPolicy( 'noindex,follow' ); + $this->setArticleRelated( false ); + $this->enableClientCache( false ); + $this->mRedirect = ''; + $this->clearHTML(); + $this->setStatusCode(503); + $this->addWikiMsg( 'actionthrottledtext' ); + + $this->returnToMain( false, $wgTitle ); } /** @@ -1327,5 +1425,72 @@ class OutputPage { $this->addHtml( "
    \n{$warning}\n
    \n" ); } } - + + /** + * Add a wikitext-formatted message to the output. + * This is equivalent to: + * + * $wgOut->addWikiText( wfMsgNoTrans( ... ) ) + */ + public function addWikiMsg( /*...*/ ) { + $args = func_get_args(); + $name = array_shift( $args ); + $this->addWikiMsgArray( $name, $args ); + } + + /** + * Add a wikitext-formatted message to the output. + * Like addWikiMsg() except the parameters are taken as an array + * instead of a variable argument list. + * + * $options is passed through to wfMsgExt(), see that function for details. + */ + public function addWikiMsgArray( $name, $args, $options = array() ) { + $options[] = 'parse'; + $text = wfMsgExt( $name, $options, $args ); + $this->addHTML( $text ); + } + + /** + * This function takes a number of message/argument specifications, wraps them in + * some overall structure, and then parses the result and adds it to the output. + * + * In the $wrap, $1 is replaced with the first message, $2 with the second, and so + * on. The subsequent arguments may either be strings, in which case they are the + * message names, or an arrays, in which case the first element is the message name, + * and subsequent elements are the parameters to that message. + * + * The special named parameter 'options' in a message specification array is passed + * through to the $options parameter of wfMsgExt(). + * + * For example: + * + * $wgOut->wrapWikiMsg( '
    $1
    ', 'some-error' ); + * + * Is equivalent to: + * + * $wgOut->addWikiText( '
    ' . wfMsgNoTrans( 'some-error' ) . '
    ' ); + */ + public function wrapWikiMsg( $wrap /*, ...*/ ) { + $msgSpecs = func_get_args(); + array_shift( $msgSpecs ); + $msgSpecs = array_values( $msgSpecs ); + $s = $wrap; + foreach ( $msgSpecs as $n => $spec ) { + $options = array(); + if ( is_array( $spec ) ) { + $args = $spec; + $name = array_shift( $args ); + if ( isset( $args['options'] ) ) { + $options = $args['options']; + unset( $args['options'] ); + } + } else { + $args = array(); + $name = $spec; + } + $s = str_replace( '$' . ($n+1), wfMsgExt( $name, $options, $args ), $s ); + } + $this->addHTML( $this->parse( $s ) ); + } } diff --git a/includes/PageHistory.php b/includes/PageHistory.php index d84c3515..0c44682e 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -61,18 +61,17 @@ class PageHistory { /* * Setup page variables. */ - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $wgOut->setPageTitle( wfMsg( 'history-title', $this->mTitle->getPrefixedText() ) ); $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); $wgOut->setArticleFlag( false ); $wgOut->setArticleRelated( true ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setSyndicated( true ); + $wgOut->setFeedAppendQuery( 'action=history' ); $logPage = SpecialPage::getTitleFor( 'Log' ); $logLink = $this->mSkin->makeKnownLinkObj( $logPage, wfMsgHtml( 'viewpagelogs' ), 'page=' . $this->mTitle->getPrefixedUrl() ); - - $subtitle = wfMsgHtml( 'revhistory' ) . '
    ' . $logLink; - $wgOut->setSubtitle( $subtitle ); + $wgOut->setSubtitle( $logLink ); $feedType = $wgRequest->getVal( 'feed' ); if( $feedType ) { @@ -84,12 +83,11 @@ class PageHistory { * Fail if article doesn't exist. */ if( !$this->mTitle->exists() ) { - $wgOut->addWikiText( wfMsg( 'nohistory' ) ); + $wgOut->addWikiMsg( 'nohistory' ); wfProfileOut( $fname ); return; } - /* * "go=first" means to jump to the last (earliest) history page. * This is deprecated, it no longer appears in the user interface @@ -99,7 +97,7 @@ class PageHistory { $wgOut->redirect( $wgTitle->getLocalURL( "action=history&limit={$limit}&dir=prev" ) ); return; } - + wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) ); /** @@ -117,7 +115,11 @@ class PageHistory { wfProfileOut( $fname ); } - /** @todo document */ + /** + * Creates begin of history list with a submit button + * + * @return string HTML output + */ function beginHistoryList() { global $wgTitle; $this->lastdate = ''; @@ -143,7 +145,11 @@ class PageHistory { return $s; } - /** @todo document */ + /** + * Creates end of history list with a submit button + * + * @return string HTML output + */ function endHistoryList() { $s = '
'; $s .= $this->submitButton( array( 'id' => 'historysubmit' ) ); @@ -151,18 +157,25 @@ class PageHistory { return $s; } - /** @todo document */ + /** + * Creates a submit button + * + * @param array $bits optional CSS ID + * @return string HTML output for the submit button + */ function submitButton( $bits = array() ) { - return ( $this->linesonpage > 0 ) - ? wfElement( 'input', array_merge( $bits, - array( + # Disable submit button if history has 1 revision only + if ( $this->linesonpage > 1 ) { + return Xml::submitButton( wfMsg( 'compareselectedversions' ), + $bits + array( 'class' => 'historysubmit', - 'type' => 'submit', 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ), - 'title' => wfMsg( 'tooltip-compareselectedversions' ).' ['.wfMsg( 'accesskey-compareselectedversions' ).']', - 'value' => wfMsg( 'compareselectedversions' ), - ) ) ) - : ''; + 'title' => wfMsg( 'tooltip-compareselectedversions' ), + ) + ); + } else { + return ''; + } } /** @@ -222,11 +235,11 @@ class PageHistory { $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - if (!is_null($size = $rev->getSize())) { - if ($size == 0) - $stxt = wfMsgHtml('historyempty'); + if ( !is_null( $size = $rev->getSize() ) ) { + if ( $size == 0 ) + $stxt = wfMsgHtml( 'historyempty' ); else - $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) ); + $stxt = wfMsgExt( 'historysize', array( 'parsemag' ), $wgLang->formatNum( $size ) ); $s .= " $stxt"; } @@ -249,18 +262,22 @@ class PageHistory { $tools = array(); if ( !is_null( $next ) && is_object( $next ) ) { - if( $wgUser->isAllowed( 'rollback' ) && $latest ) { + if( !$this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) + && !$this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ) + && $latest ) { $tools[] = '' . $this->mSkin->buildRollbackLink( $rev ) . ''; } - $undolink = $this->mSkin->makeKnownLinkObj( - $this->mTitle, - wfMsgHtml( 'editundo' ), - 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() - ); - $tools[] = "{$undolink}"; + if( $this->mTitle->quickUserCan( 'edit' ) ) { + $undolink = $this->mSkin->makeKnownLinkObj( + $this->mTitle, + wfMsgHtml( 'editundo' ), + 'action=edit&undoafter=' . $next->rev_id . '&undo=' . $rev->getId() + ); + $tools[] = "{$undolink}"; + } } if( $tools ) { @@ -329,14 +346,19 @@ class PageHistory { } } - /** @todo document */ + /** + * Create radio buttons for page history + * + * @param object $rev Revision + * @param bool $firstInList Is this version the first one? + * @param int $counter A counter of what row number we're at, counted from the top row = 1. + * @return string HTML output for the radio buttons + */ function diffButtons( $rev, $firstInList, $counter ) { if( $this->linesonpage > 1) { $radio = array( 'type' => 'radio', 'value' => $rev->getId(), -# do we really need to flood this on every item? -# 'title' => wfMsgHtml( 'selectolderversionfordiff' ) ); if( !$rev->userCan( Revision::DELETED_TEXT ) ) { @@ -345,7 +367,7 @@ class PageHistory { /** @todo: move title texts to javascript */ if ( $firstInList ) { - $first = wfElement( 'input', array_merge( + $first = Xml::element( 'input', array_merge( $radio, array( 'style' => 'visibility:hidden', @@ -357,13 +379,13 @@ class PageHistory { } else { $checkmark = array(); } - $first = wfElement( 'input', array_merge( + $first = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'oldid' ) ) ); $checkmark = array(); } - $second = wfElement( 'input', array_merge( + $second = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'diff' ) ) ); @@ -464,7 +486,7 @@ class PageHistory { global $wgFeedClasses; if( !isset( $wgFeedClasses[$type] ) ) { global $wgOut; - $wgOut->addWikiText( wfMsg( 'feed-invalid' ) ); + $wgOut->addWikiMsg( 'feed-invalid' ); return; } @@ -607,6 +629,3 @@ class PageHistoryPager extends ReverseChronologicalPager { return $s; } } - - - diff --git a/includes/Pager.php b/includes/Pager.php index 70d0873c..ed7086b4 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -422,21 +422,21 @@ abstract class AlphabeticPager extends IndexPager { */ function getNavigationBar() { global $wgLang; - + $linkTexts = array( - 'prev' => wfMsgHtml( "prevn", $this->mLimit ), - 'next' => wfMsgHtml( 'nextn', $this->mLimit ), - 'first' => wfMsgHtml('page_first'), /* Introduced the message */ + 'prev' => wfMsgHtml( 'prevn', $wgLang->formatNum( $this->mLimit ) ), + 'next' => wfMsgHtml( 'nextn', $wgLang->formatNum($this->mLimit ) ), + 'first' => wfMsgHtml( 'page_first' ), /* Introduced the message */ 'last' => wfMsgHtml( 'page_last' ) /* Introduced the message */ ); - + $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; - + } } @@ -457,17 +457,18 @@ abstract class ReverseChronologicalPager extends IndexPager { if ( isset( $this->mNavigationBar ) ) { return $this->mNavigationBar; } + $nicenumber = $wgLang->formatNum( $this->mLimit ); $linkTexts = array( - 'prev' => wfMsgHtml( "prevn", $this->mLimit ), - 'next' => wfMsgHtml( 'nextn', $this->mLimit ), - 'first' => wfMsgHtml('histlast'), + 'prev' => wfMsgExt( 'pager-newer-n', array( 'parsemag' ), $nicenumber ), + 'next' => wfMsgExt( 'pager-older-n', array( 'parsemag' ), $nicenumber ), + 'first' => wfMsgHtml( 'histlast' ), 'last' => wfMsgHtml( 'histfirst' ) ); $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); $limits = implode( ' | ', $limitLinks ); - + $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; @@ -712,4 +713,3 @@ abstract class TablePager extends IndexPager { */ abstract function getFieldNames(); } - diff --git a/includes/Parser.php b/includes/Parser.php index 32e7f2a8..41eabe4f 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -7,55 +7,6 @@ * @addtogroup Parser */ -/** - * Update this version number when the ParserOutput format - * changes in an incompatible way, so the parser cache - * can automatically discard old data. - */ -define( 'MW_PARSER_VERSION', '1.6.2' ); - -define( 'RLH_FOR_UPDATE', 1 ); - -# Allowed values for $mOutputType -define( 'OT_HTML', 1 ); -define( 'OT_WIKI', 2 ); -define( 'OT_MSG' , 3 ); -define( 'OT_PREPROCESS', 4 ); - -# Flags for setFunctionHook -define( 'SFH_NO_HASH', 1 ); - -# string parameter for extractTags which will cause it -# to strip HTML comments in addition to regular -# -style tags. This should not be anything we -# may want to use in wikisyntax -define( 'STRIP_COMMENTS', 'HTMLCommentStrip' ); - -# Constants needed for external link processing -define( 'HTTP_PROTOCOLS', 'http:\/\/|https:\/\/' ); -# Everything except bracket, space, or control characters -define( 'EXT_LINK_URL_CLASS', '[^][<>"\\x00-\\x20\\x7F]' ); -# Including space, but excluding newlines -define( 'EXT_LINK_TEXT_CLASS', '[^\]\\x0a\\x0d]' ); -define( 'EXT_IMAGE_FNAME_CLASS', '[A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]' ); -define( 'EXT_IMAGE_EXTENSIONS', 'gif|png|jpg|jpeg' ); -define( 'EXT_LINK_BRACKETED', '/\[(\b(' . wfUrlProtocols() . ')'. - EXT_LINK_URL_CLASS.'+) *('.EXT_LINK_TEXT_CLASS.'*?)\]/S' ); -define( 'EXT_IMAGE_REGEX', - '/^('.HTTP_PROTOCOLS.')'. # Protocol - '('.EXT_LINK_URL_CLASS.'+)\\/'. # Hostname and path - '('.EXT_IMAGE_FNAME_CLASS.'+)\\.((?i)'.EXT_IMAGE_EXTENSIONS.')$/S' # Filename -); - -// State constants for the definition list colon extraction -define( 'MW_COLON_STATE_TEXT', 0 ); -define( 'MW_COLON_STATE_TAG', 1 ); -define( 'MW_COLON_STATE_TAGSTART', 2 ); -define( 'MW_COLON_STATE_CLOSETAG', 3 ); -define( 'MW_COLON_STATE_TAGSLASH', 4 ); -define( 'MW_COLON_STATE_COMMENT', 5 ); -define( 'MW_COLON_STATE_COMMENTDASH', 6 ); -define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); /** * PHP Parser - Processes wiki markup (which uses a more user-friendly @@ -64,15 +15,17 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 ); * (which in turn the browser understands, and can display). * *
- * There are four main entry points into the Parser class:
+ * There are five main entry points into the Parser class:
  * parse()
  *   produces HTML output
  * preSaveTransform().
  *   produces altered wiki markup.
- * transformMsg()
- *   performs brace substitution on MediaWiki messages
  * 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
@@ -92,23 +45,60 @@ define( 'MW_COLON_STATE_COMMENTDASHDASH', 7 );
  */
 class Parser
 {
-	const VERSION = MW_PARSER_VERSION;
+	/**
+	 * 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;
+	
 	/**#@+
 	 * @private
 	 */
 	# Persistent:
 	var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
-		$mImageParams, $mImageParamsMagicArray;
-	
+		$mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerSuffix, $mMarkerIndex,
+		$mExtLinkBracketedRegex, $mPreprocessor, $mDefaultStripList, $mVarCache, $mConf;
+
+
 	# 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.
+	var $mInterwikiLinkHolders, $mLinkHolders;
+	var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
+	var $mTplExpandCache; // empty-frame expansion cache
+	var $mTplRedirCache, $mTplDomCache, $mHeadings;
 
 	# Temporary
 	# These are variables reset at least once per parse regardless of $clearState
@@ -127,11 +117,23 @@ class Parser
 	 *
 	 * @public
 	 */
-	function Parser() {
+	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->mMarkerSuffix = "-QINU\x7f";
+		$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_DOM';
+		}
+		$this->mMarkerIndex = 0;
 		$this->mFirstCall = true;
 	}
 	
@@ -142,38 +144,46 @@ class Parser
 		if ( !$this->mFirstCall ) {
 			return;
 		}
+		$this->mFirstCall = false;
 		
 		wfProfileIn( __METHOD__ );
 		global $wgAllowDisplayTitle, $wgAllowSlowParserFunctions;
-		
+
 		$this->setHook( 'pre', array( $this, 'renderPreTag' ) );
-		
-		$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 );
+
+		# 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( '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 );
+		$this->setFunctionHook( 'tag',              array( 'CoreParserFunctions', 'tagObj'           ), SFH_OBJECT_ARGS );
 
 		if ( $wgAllowDisplayTitle ) {
 			$this->setFunctionHook( 'displaytitle', array( 'CoreParserFunctions', 'displaytitle' ), SFH_NO_HASH );
@@ -183,7 +193,8 @@ class Parser
 		}
 
 		$this->initialiseVariables();
-		$this->mFirstCall = false;
+
+		wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
 		wfProfileOut( __METHOD__ );
 	}
 
@@ -203,7 +214,7 @@ class Parser
 		$this->mDTopen = false;
 		$this->mIncludeCount = array();
 		$this->mStripState = new StripState;
-		$this->mArgStack = array();
+		$this->mArgStack = false;
 		$this->mInPre = false;
 		$this->mInterwikiLinkHolders = array(
 			'texts' => array(),
@@ -224,21 +235,32 @@ class Parser
 		 * 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  in a link.
 		 */
-		$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
+		#$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->mTemplates = array();
- 		$this->mTemplatePath = array();
+		$this->mTplExpandCache = $this->mTplRedirCache = $this->mTplDomCache = array();
 
 		$this->mShowToc = true;
 		$this->mForceTocPosition = false;
 		$this->mIncludeSizes = array(
-			'pre-expand' => 0,
 			'post-expand' => 0,
-			'arg' => 0
+			'arg' => 0,
 		);
+		$this->mPPNodeCount = 0;
 		$this->mDefaultSort = false;
+		$this->mHeadings = array();
+
+		# Fix cloning
+		if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
+			$this->mPreprocessor = null;
+		}
 
 		wfRunHooks( 'ParserClearState', array( &$this ) );
 		wfProfileOut( __METHOD__ );
@@ -248,19 +270,43 @@ class Parser
 		$this->mOutputType = $ot;
 		// Shortcut alias
 		$this->ot = array(
-			'html' => $ot == OT_HTML,
-			'wiki' => $ot == OT_WIKI,
-			'msg' => $ot == OT_MSG,
-			'pre' => $ot == OT_PREPROCESS,
+			'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;
 	}
 
@@ -292,16 +338,16 @@ class Parser
 		}
 
 		$this->mOptions = $options;
-		$this->mTitle =& $title;
+		$this->setTitle( $title );
 		$oldRevisionId = $this->mRevisionId;
 		$oldRevisionTimestamp = $this->mRevisionTimestamp;
 		if( $revid !== null ) {
 			$this->mRevisionId = $revid;
 			$this->mRevisionTimestamp = null;
 		}
-		$this->setOutputType( OT_HTML );
+		$this->setOutputType( self::OT_HTML );
 		wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
-		$text = $this->strip( $text, $this->mStripState );
+		# No more strip!
 		wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
 		$text = $this->internalParse( $text );
 		$text = $this->mStripState->unstripGeneral( $text );
@@ -334,17 +380,17 @@ class Parser
 //!JF Move to its own function
 
 		$uniq_prefix = $this->mUniqPrefix;
-                $matches = array();
+		$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 {
+		$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 );
@@ -386,14 +432,15 @@ class Parser
 		wfRunHooks( 'ParserAfterTidy', array( &$this, &$text ) );
 
 		# Information on include size limits, for the benefit of users who try to skirt them
-		if ( max( $this->mIncludeSizes ) > 1000 ) {
+		if ( $this->mOptions->getEnableLimitReport() ) {
 			$max = $this->mOptions->getMaxIncludeSize();
-			$text .= "\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";
+			wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
+			$text .= "\n\n";
 		}
 		$this->mOutput->setText( $text );
 		$this->mRevisionId = $oldRevisionId;
@@ -411,7 +458,6 @@ class Parser
 	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__ );
@@ -425,18 +471,14 @@ class Parser
 	function preprocess( $text, $title, $options, $revid = null ) {
 		wfProfileIn( __METHOD__ );
 		$this->clearState();
-		$this->setOutputType( OT_PREPROCESS );
+		$this->setOutputType( self::OT_PREPROCESS );
 		$this->mOptions = $options;
-		$this->mTitle = $title;
+		$this->setTitle( $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__ );
@@ -461,9 +503,20 @@ class Parser
 		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 teh next text. The output
+	 * 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(
@@ -507,7 +560,7 @@ class Parser
 				$inside     = $p[4];
 			}
 
-			$marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07";
+			$marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . $this->mMarkerSuffix;
 			$stripped .= $marker;
 
 			if ( $close === '/>' ) {
@@ -542,125 +595,24 @@ class Parser
 	}
 
 	/**
-	 * 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 
-	 *  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  when saving (fixes bug 2700)
-	 *
-	 * @private
+	 * Get a list of strippable XML-like elements
 	 */
-	function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
-		global $wgContLang;
-		wfProfileIn( __METHOD__ );
-		$render = ($this->mOutputType == OT_HTML);
-
-		$uniq_prefix = $this->mUniqPrefix;
-		$commentState = new ReplacementArray;
-		$nowikiItems = array();
-		$generalItems = array();
-
-		$elements = array_merge(
-			array( 'nowiki', 'gallery' ),
-			array_keys( $this->mTagHooks ) );
+	function getStripList() {
 		global $wgRawHtml;
+		$elements = $this->mStripList;
 		if( $wgRawHtml ) {
 			$elements[] = 'html';
 		}
 		if( $this->mOptions->getUseTeX() ) {
 			$elements[] = 'math';
 		}
+		return $elements;
+	}
 
-		# 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 = Parser::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__ );
+	/**
+	 * @deprecated use replaceVariables
+	 */
+	function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
 		return $text;
 	}
 
@@ -699,9 +651,10 @@ class Parser
 	 *
 	 * @private
 	 */
-	function insertStripItem( $text, &$state ) {
-		$rnd = $this->mUniqPrefix . '-item' . Parser::getRandomString();
-		$state->general->setPair( $rnd, $text );
+	function insertStripItem( $text ) {
+		$rnd = "{$this->mUniqPrefix}-item-{$this->mMarkerIndex}-{$this->mMarkerSuffix}";
+		$this->mMarkerIndex++;
+		$this->mStripState->general->setPair( $rnd, $text );
 		return $rnd;
 	}
 
@@ -785,8 +738,7 @@ class Parser
 
 	/**
 	 * Use the HTML tidy PECL extension to use the tidy library in-process,
-	 * saving the overhead of spawning a new process. Currently written to
-	 * the PHP 4.3.x version of the extension, may not work on PHP 5.
+	 * saving the overhead of spawning a new process. 
 	 *
 	 * 'pear install tidy' should be able to compile the extension module.
 	 *
@@ -794,21 +746,26 @@ class Parser
 	 * @static
 	 */
 	function internalTidy( $text ) {
-		global $wgTidyConf;
+		global $wgTidyConf, $IP, $wgDebugTidy;
 		$fname = 'Parser::internalTidy';
 		wfProfileIn( $fname );
 
-		tidy_load_config( $wgTidyConf );
-		tidy_set_encoding( 'utf8' );
-		tidy_parse_string( $text );
-		tidy_clean_repair();
-		if( tidy_get_status() == 2 ) {
+		$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();
+			$cleansource = tidy_get_output( $tidy );
+		}
+		if ( $wgDebugTidy && $tidy->getStatus() > 0 ) {
+			$cleansource .= "', '-->', $tidy->errorBuffer ) . 
+				"\n-->";
 		}
+
 		wfProfileOut( $fname );
 		return $cleansource;
 	}
@@ -1007,12 +964,11 @@ class Parser
 
 	/**
 	 * Helper function for parse() that transforms wiki markup into
-	 * HTML. Only called for $mOutputType == OT_HTML.
+	 * HTML. Only called for $mOutputType == self::OT_HTML.
 	 *
 	 * @private
 	 */
 	function internalParse( $text ) {
-		$args = array();
 		$isMain = true;
 		$fname = 'Parser::internalParse';
 		wfProfileIn( $fname );
@@ -1023,14 +979,8 @@ class Parser
 			return $text ;
 		}
 
-		# Remove  tags and  sections
-		$text = strtr( $text, array( '' => '' , '' => '' ) );
-		$text = strtr( $text, array( '' => '', '' => '') );
-		$text = StringUtils::delimiterReplace( '', '', '', $text );
-
-		$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) );
-
-		$text = $this->replaceVariables( $text, $args );
+		$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
@@ -1069,7 +1019,7 @@ class Parser
 	 *
 	 * @private
 	 */
-	function &doMagicLinks( &$text ) {
+	function doMagicLinks( $text ) {
 		wfProfileIn( __METHOD__ );
 		$text = preg_replace_callback(
 			'!(?:                           # Start cases
@@ -1133,8 +1083,8 @@ class Parser
 		wfProfileIn( $fname );
 		for ( $i = 6; $i >= 1; --$i ) {
 			$h = str_repeat( '=', $i );
-			$text = preg_replace( "/^{$h}(.+){$h}\\s*$/m",
-			  "\\1\\2", $text );
+			$text = preg_replace( "/^$h(.+)$h\\s*$/m",
+			  "\\1", $text );
 		}
 		wfProfileOut( $fname );
 		return $text;
@@ -1160,9 +1110,8 @@ class Parser
 
 	/**
 	 * Helper function for doAllQuotes()
-	 * @private
 	 */
-	function doQuotes( $text ) {
+	public function doQuotes( $text ) {
 		$arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
 		if ( count( $arr ) == 1 )
 			return $text;
@@ -1339,7 +1288,7 @@ class Parser
 
 		$sk = $this->mOptions->getSkin();
 
-		$bits = preg_split( EXT_LINK_BRACKETED, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
 
 		$s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
 
@@ -1433,7 +1382,7 @@ class Parser
 			$remainder = $bits[$i++];
 
 			$m = array();
-			if ( preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
+			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];
@@ -1443,7 +1392,7 @@ class Parser
 				if(strlen($trail) == 0 &&
 					isset($bits[$i]) &&
 					preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
-					preg_match( '/^('.EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
+					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
@@ -1540,7 +1489,7 @@ class Parser
 		$text = false;
 		if ( $this->mOptions->getAllowExternalImages()
 		     || ( $imagesexception && strpos( $url, $imagesfrom ) === 0 ) ) {
-			if ( preg_match( EXT_IMAGE_REGEX, $url ) ) {
+			if ( preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
 				# Image found
 				$text = $sk->makeExternalImage( htmlspecialchars( $url ) );
 			}
@@ -1578,11 +1527,15 @@ class Parser
 		# 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();
+		$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 ) ) {
 			throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
 		}
@@ -2283,7 +2236,7 @@ class Parser
 		}
 
 		// Ugly state machine to walk through avoiding tags.
-		$state = MW_COLON_STATE_TEXT;
+		$state = self::COLON_STATE_TEXT;
 		$stack = 0;
 		$len = strlen( $str );
 		for( $i = 0; $i < $len; $i++ ) {
@@ -2291,11 +2244,11 @@ class Parser
 
 			switch( $state ) {
 			// (Using the number is a performance hack for common cases)
-			case 0: // MW_COLON_STATE_TEXT:
+			case 0: // self::COLON_STATE_TEXT:
 				switch( $c ) {
 				case "<":
 					// Could be either a  tag or an  tag
-					$state = MW_COLON_STATE_TAGSTART;
+					$state = self::COLON_STATE_TAGSTART;
 					break;
 				case ":":
 					if( $stack == 0 ) {
@@ -2332,41 +2285,41 @@ class Parser
 					}
 					// Skip ahead to next tag start
 					$i = $lt;
-					$state = MW_COLON_STATE_TAGSTART;
+					$state = self::COLON_STATE_TAGSTART;
 				}
 				break;
-			case 1: // MW_COLON_STATE_TAG:
+			case 1: // self::COLON_STATE_TAG:
 				// In a 
 				switch( $c ) {
 				case ">":
 					$stack++;
-					$state = MW_COLON_STATE_TEXT;
+					$state = self::COLON_STATE_TEXT;
 					break;
 				case "/":
 					// Slash may be followed by >?
-					$state = MW_COLON_STATE_TAGSLASH;
+					$state = self::COLON_STATE_TAGSLASH;
 					break;
 				default:
 					// ignore
 				}
 				break;
-			case 2: // MW_COLON_STATE_TAGSTART:
+			case 2: // self::COLON_STATE_TAGSTART:
 				switch( $c ) {
 				case "/":
-					$state = MW_COLON_STATE_CLOSETAG;
+					$state = self::COLON_STATE_CLOSETAG;
 					break;
 				case "!":
-					$state = MW_COLON_STATE_COMMENT;
+					$state = self::COLON_STATE_COMMENT;
 					break;
 				case ">":
 					// Illegal early close? This shouldn't happen D:
-					$state = MW_COLON_STATE_TEXT;
+					$state = self::COLON_STATE_TEXT;
 					break;
 				default:
-					$state = MW_COLON_STATE_TAG;
+					$state = self::COLON_STATE_TAG;
 				}
 				break;
-			case 3: // MW_COLON_STATE_CLOSETAG:
+			case 3: // self::COLON_STATE_CLOSETAG:
 				// In a 
 				if( $c == ">" ) {
 					$stack--;
@@ -2375,35 +2328,35 @@ class Parser
 						wfProfileOut( $fname );
 						return false;
 					}
-					$state = MW_COLON_STATE_TEXT;
+					$state = self::COLON_STATE_TEXT;
 				}
 				break;
-			case MW_COLON_STATE_TAGSLASH:
+			case self::COLON_STATE_TAGSLASH:
 				if( $c == ">" ) {
 					// Yes, a self-closed tag 
-					$state = MW_COLON_STATE_TEXT;
+					$state = self::COLON_STATE_TEXT;
 				} else {
 					// Probably we're jumping the gun, and this is an attribute
-					$state = MW_COLON_STATE_TAG;
+					$state = self::COLON_STATE_TAG;
 				}
 				break;
-			case 5: // MW_COLON_STATE_COMMENT:
+			case 5: // self::COLON_STATE_COMMENT:
 				if( $c == "-" ) {
-					$state = MW_COLON_STATE_COMMENTDASH;
+					$state = self::COLON_STATE_COMMENTDASH;
 				}
 				break;
-			case MW_COLON_STATE_COMMENTDASH:
+			case self::COLON_STATE_COMMENTDASH:
 				if( $c == "-" ) {
-					$state = MW_COLON_STATE_COMMENTDASHDASH;
+					$state = self::COLON_STATE_COMMENTDASHDASH;
 				} else {
-					$state = MW_COLON_STATE_COMMENT;
+					$state = self::COLON_STATE_COMMENT;
 				}
 				break;
-			case MW_COLON_STATE_COMMENTDASHDASH:
+			case self::COLON_STATE_COMMENTDASHDASH:
 				if( $c == ">" ) {
-					$state = MW_COLON_STATE_TEXT;
+					$state = self::COLON_STATE_TEXT;
 				} else {
-					$state = MW_COLON_STATE_COMMENT;
+					$state = self::COLON_STATE_COMMENT;
 				}
 				break;
 			default:
@@ -2430,14 +2383,13 @@ class Parser
 		 * 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];
+		if ( wfRunHooks( 'ParserGetVariableValueVarCache', array( &$this, &$this->mVarCache ) ) ) {
+			if ( isset( $this->mVarCache[$index] ) ) {
+				return $this->mVarCache[$index];
 			}
 		}
 
-		$ts = time();
+		$ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
 		wfRunHooks( 'ParserGetVariableValueTs', array( &$this, &$ts ) );
 
 		# Use the time zone
@@ -2464,29 +2416,29 @@ class Parser
 
 		switch ( $index ) {
 			case 'currentmonth':
-				return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
 			case 'currentmonthname':
-				return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
 			case 'currentmonthnamegen':
-				return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
 			case 'currentmonthabbrev':
-				return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
 			case 'currentday':
-				return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
 			case 'currentday2':
-				return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
 			case 'localmonth':
-				return $varCache[$index] = $wgContLang->formatNum( $localMonth );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
 			case 'localmonthname':
-				return $varCache[$index] = $wgContLang->getMonthName( $localMonthName );
+				return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
 			case 'localmonthnamegen':
-				return $varCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
+				return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
 			case 'localmonthabbrev':
-				return $varCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
+				return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
 			case 'localday':
-				return $varCache[$index] = $wgContLang->formatNum( $localDay );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
 			case 'localday2':
-				return $varCache[$index] = $wgContLang->formatNum( $localDay2 );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 );
 			case 'pagename':
 				return wfEscapeWikiText( $this->mTitle->getText() );
 			case 'pagenamee':
@@ -2524,16 +2476,40 @@ class Parser
 				$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() ) );
@@ -2548,51 +2524,51 @@ class Parser
 			case 'subjectspacee':
 				return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
 			case 'currentdayname':
-				return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+				return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
 			case 'currentyear':
-				return $varCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
 			case 'currenttime':
-				return $varCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+				return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
 			case 'currenthour':
-				return $varCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+				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 $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
 			case 'currentdow':
-				return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
 			case 'localdayname':
-				return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+				return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
 			case 'localyear':
-				return $varCache[$index] = $wgContLang->formatNum( $localYear, true );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
 			case 'localtime':
-				return $varCache[$index] = $wgContLang->time( $localTimestamp, false, false );
+				return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
 			case 'localhour':
-				return $varCache[$index] = $wgContLang->formatNum( $localHour, true );
+				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 $varCache[$index] = $wgContLang->formatNum( (int)$localWeek );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek );
 			case 'localdow':
-				return $varCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
 			case 'numberofarticles':
-				return $varCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
 			case 'numberoffiles':
-				return $varCache[$index] = $wgContLang->formatNum( SiteStats::images() );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
 			case 'numberofusers':
-				return $varCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
 			case 'numberofpages':
-				return $varCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
 			case 'numberofadmins':
-				return $varCache[$index]  = $wgContLang->formatNum( SiteStats::admins() );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::admins() );
 			case 'numberofedits':
-				return $varCache[$index]  = $wgContLang->formatNum( SiteStats::edits() );
+				return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
 			case 'currenttimestamp':
-				return $varCache[$index] = wfTimestampNow();
+				return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
 			case 'localtimestamp':
-				return $varCache[$index] = $localTimestamp;
+				return $this->mVarCache[$index] = $localTimestamp;
 			case 'currentversion':
-				return $varCache[$index] = SpecialVersion::getVersion();
+				return $this->mVarCache[$index] = SpecialVersion::getVersion();
 			case 'sitename':
 				return $wgSitename;
 			case 'server':
@@ -2608,7 +2584,7 @@ class Parser
 				return $wgContLanguageCode;
 			default:
 				$ret = null;
-				if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$varCache, &$index, &$ret ) ) )
+				if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) )
 					return $ret;
 				else
 					return null;
@@ -2625,187 +2601,51 @@ class Parser
 		wfProfileIn( $fname );
 		$variableIDs = MagicWord::getVariableIDs();
 
-		$this->mVariables = array();
-		foreach ( $variableIDs as $id ) {
-			$mw =& MagicWord::get( $id );
-			$mw->addToArray( $this->mVariables, $id );
-		}
+		$this->mVariables = new MagicWordArray( $variableIDs );
 		wfProfileOut( $fname );
 	}
 
 	/**
-	 * parse any parentheses in format ((title|part|part))
-	 * and call callbacks to get a replacement text for any found piece
+	 * Preprocess some wikitext and return the document tree.
+	 * This is the ghost of replace_variables(). 
 	 *
 	 * @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
+	 * @param integer flags Bitwise combination of:
+	 *          self::PTD_FOR_INCLUSION    Handle / 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 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;
-				}
+	function preprocessToDom ( $text, $flags = 0 ) {
+		$dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
+		return $dom;
+	}
 
-				# 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;
-			}
+	/* 
+	 * 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 = '';
 		}
-
-		wfProfileOut( __METHOD__ );
-		return $text;
+		return array( $w1, $trimmed, $w2 );
 	}
 
 	/**
@@ -2814,94 +2654,38 @@ class Parser
 	 * taking care to avoid infinite loops.
 	 *
 	 * Note that the substitution depends on value of $mOutputType:
-	 *  OT_WIKI: only {{subst:}} templates
-	 *  OT_MSG: only magic variables
-	 *  OT_HTML: all templates and magic variables
+	 *  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 array $args Key-value pairs representing template parameters to substitute
+	 * @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, $args = array(), $argsOnly = false ) {
+	function replaceVariables( $text, $frame = false, $argsOnly = false ) {
 		# Prevent too big inclusions
 		if( strlen( $text ) > $this->mOptions->getMaxIncludeSize() ) {
 			return $text;
 		}
 
-		$fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
+		$fname = __METHOD__;
 		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 != 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 );
+		if ( $frame === false ) {
+			$frame = $this->getPreprocessor()->newFrame();
+		} elseif ( !( $frame instanceof PPFrame ) ) {
+			throw new MWException( __METHOD__ . ' called using the old argument format' );
 		}
-		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 == 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 );
-				$this->mOutput->mContainsOldMagic = true;
-			} else {
-				$text = $matches[0];
-			}
-		} else {
-			$text = $matches[0];
-		}
+		$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();
@@ -2930,50 +2714,40 @@ class Parser
 	 * 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
+	 *  $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 ) {
+	function braceSubstitution( $piece, $frame ) {
 		global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
-		$fname = __METHOD__ /*. '-L' . count( $this->mArgStack )*/;
+		$fname = __METHOD__;
 		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
+		$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;
 
-		$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
+		# $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;
 
-		$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;
-			}
-		}
+		# 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' );
 
@@ -2986,10 +2760,20 @@ class Parser
 				# 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'];
+				$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;
-				$noparse = true;
-				$noargs = true;
 			}
 		}
 
@@ -3013,9 +2797,6 @@ class Parser
 		}
 		wfProfileOut( __METHOD__.'-modifiers' );
 
-		//save path level before recursing into functions & templates.
-		$lastPathLevel = $this->mTemplatePath;
-
 		# Parser functions
 		if ( !$found ) {
 			wfProfileIn( __METHOD__ . '-pfunc' );
@@ -3036,288 +2817,278 @@ class Parser
 					}
 				}
 				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;
+					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 );
+					}
 
-					// The text is usually already parsed, doesn't need triple-brace tags expanded, etc.
-					//$noargs = true;
-					//$noparse = true;
+					# 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;
 
 					if ( is_array( $result ) ) {
 						if ( isset( $result[0] ) ) {
-							$text = $linestart . $result[0];
+							$text = $result[0];
 							unset( $result[0] );
 						}
 
 						// Extract flags into the local scope
-						// This allows callers to set flags such as nowiki, noparse, found, etc.
+						// This allows callers to set flags such as nowiki, found, etc.
 						extract( $result );
 					} else {
-						$text = $linestart . $result;
+						$text = $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]]";
-				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
+		# Finish mangling title and then check for loops.
+		# Set $title to a Title object and $titleText to the PDBK
 		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
+			# 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 ( !is_null( $title ) ) {
+			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 = "Template loop detected: [[$titleText]]";
+					wfDebug( __METHOD__.": template loop broken at '$titleText'\n" );
+				}
+				# Do recursion depth check
+				$limit = $this->mOptions->getMaxTemplateDepth();
+				if ( $frame->depth >= $limit ) {
+					$found = true;
+					$text = "Template recursion depth limit exceeded ($limit)";
+				}
+			}
+		}
 
-				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]]";
+		# 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;
-					}
-				} 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;
+						$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;
 					}
-					$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;
+				# 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 ( $found && !$this->incrementIncludeSize( 'pre-expand', strlen( $text ) ) ) {
-			# Error, oversize inclusion
-			$text = $linestart .
-				"[[$titleText]]";
-			$noparse = true;
-			$noargs = true;
+		# 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 );
 		}
 
-		# 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  tags, only include them
-				if ( in_string( '', $text ) && in_string( '', $text ) ) {
-					$replacer = new OnlyIncludeReplacer;
-					StringUtils::delimiterReplaceCallback( '', '', 
-						array( &$replacer, 'replace' ), $text );
-					$text = $replacer->output;
-				}
-				# Remove  sections and  tags
-				$text = StringUtils::delimiterReplace( '', '', '', $text );
-				$text = strtr( $text, array( '' => '' , '' => '' ) );
-
-				if( $this->ot['html'] || $this->ot['pre'] ) {
-					# Strip , 
, 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;
+		# 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;
 				}
-			} 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 );
+			} else {
+				# Uncached expansion
+				$text = $newFrame->expand( $text );
 			}
 		}
-		# Prune lower levels off the recursion check path
-		$this->mTemplatePath = $lastPathLevel;
+		if ( $isLocalObj && $nowiki ) {
+			$text = $frame->expand( $text, PPFrame::RECOVER_ORIG );
+			$isLocalObj = false;
+		}
 
-		if ( $found && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
+		# 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 = $linestart .
-				"[[$titleText]]";
-			$noparse = true;
-			$noargs = true;
+			$text = "[[$originalTitle]]" . 
+				$this->insertStripItem( '' );
 		}
 
-		if ( !$found ) {
-			wfProfileOut( $fname );
-			return $piece['text'];
+		if ( $isLocalObj ) {
+			$ret = array( 'object' => $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, "" . $m2[3];
+			$ret = array( 'text' => $text );
+		}
 
-						$nsec++;
-					}
-				}
-			}
-			wfProfileOut( __METHOD__ . '-placeholders' );
+		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 );
 		}
 
-		# Prune lower levels off the recursion check path
-		$this->mTemplatePath = $lastPathLevel;
+		// Cache miss, go to the database
+		list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
 
-		if ( !$found ) {
-			wfProfileOut( $fname );
-			return $piece['text'];
-		} else {
-			wfProfileOut( $fname );
-			return $text;
+		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 ) {
+	function fetchTemplateAndTitle( $title ) {
+		$templateCb = $this->mOptions->getTemplateCallback();
+		$stuff = call_user_func( $templateCb, $title );
+		$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 ) {
 		$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( &$this, &$title, &$skip, &$id ) );
+			wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( false, &$title, &$skip, &$id ) );
 			
 			if( $skip ) {
 				$text = false;
-				$this->mOutput->addTemplate( $title, $title->getArticleID(), null );
+				$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;
-			
-			$this->mOutput->addTemplate( $title, $title->getArticleID(), $rev_id );
-			
+
+			$deps[] = array( 
+				'title' => $title, 
+				'page_id' => $title->getArticleID(), 
+				'rev_id' => $rev_id );
+
 			if( $rev ) {
 				$text = $rev->getText();
 			} elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
@@ -3338,12 +3109,10 @@ class Parser
 			$finalTitle = $title;
 			$title = Title::newFromRedirect( $text );
 		}
-		return array($text,$finalTitle);
-	}
-
-	function fetchTemplate( $title ) {
-		$rv = $this->fetchTemplateAndtitle($title);
-		return $rv[0];
+		return array(
+			'text' => $text,
+			'finalTitle' => $finalTitle,
+			'deps' => $deps );
 	}
 
 	/**
@@ -3392,23 +3161,128 @@ class Parser
 	 * Triple brace replacement -- used for template arguments
 	 * @private
 	 */
-	function argSubstitution( $matches ) {
-		$arg = trim( $matches['title'] );
-		$text = $matches['text'];
-		$inputArgs = end( $this->mArgStack );
+	function argSubstitution( $piece, $frame ) {
+		wfProfileIn( __METHOD__ );
 
-		if ( array_key_exists( $arg, $inputArgs ) ) {
-			$text = $inputArgs[$arg];
-		} else if (($this->mOutputType == OT_HTML || $this->mOutputType == OT_PREPROCESS ) &&
-		null != $matches['parts'] && count($matches['parts']) > 0) {
-			$text = $matches['parts'][0];
+		$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 ) ) ) {
-			$text = $matches['text'] .
-				'';
+			$error = '';
 		}
 
-		return $text;
+		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++) . $this->mMarkerSuffix;
+		
+		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( ' 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 {
+						throw new MWException( "Invalid call hook $name" );
+					}
+			}
+		} 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;
 	}
 
 	/**
@@ -3419,7 +3293,7 @@ class Parser
 	 * @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() ) {
+		if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) {
 			return false;
 		} else {
 			$this->mIncludeSizes[$type] += $size;
@@ -3471,7 +3345,7 @@ class Parser
 	/**
 	 * This function accomplishes several tasks:
 	 * 1) Auto-number headings if that option is enabled
-	 * 2) Add an [edit] link to sections for logged in users who have enabled the option
+	 * 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
 	 *
@@ -3527,7 +3401,6 @@ class Parser
 
 		# headline counter
 		$headlineCount = 0;
-		$sectionCount = 0; # headlineCount excluding template sections
 		$numVisible = 0;
 
 		# Ugh .. the TOC should have neat indentation levels which can be
@@ -3542,18 +3415,21 @@ class Parser
 		$prevlevel = 0;
 		$toclevel = 0;
 		$prevtoclevel = 0;
+		$markerRegex = "{$this->mUniqPrefix}-h-(\d+)-{$this->mMarkerSuffix}";
+		$baseTitleText = $this->mTitle->getPrefixedDBkey();
+		$tocraw = array();
 
 		foreach( $matches[3] as $headline ) {
-			$istemplate = 0;
-			$templatetitle = '';
-			$templatesection = 0;
+			$isTemplate = false;
+			$titleText = false;
+			$sectionIndex = false;
 			$numbering = '';
-			$mat = array();
-			if (preg_match("//", $headline, $mat)) {
-				$istemplate = 1;
-				$templatetitle = base64_decode($mat[1]);
-				$templatesection = 1 + (int)base64_decode($mat[2]);
-				$headline = preg_replace("//", "", $headline);
+			$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 ) {
@@ -3626,41 +3502,41 @@ class Parser
 				}
 			}
 
-			# The canonized header is a version of the header text safe to use for links
+			# The safe header is a version of the header text safe to use for links
 			# Avoid insertion of weird stuff like  by expanding the relevant sections
-			$canonized_headline = $this->mStripState->unstripBoth( $headline );
+			$safeHeadline = $this->mStripState->unstripBoth( $headline );
 
 			# Remove link placeholders by the link text.
 			#     
 			# turns into
 			#     link text with suffix
-			$canonized_headline = preg_replace( '//e',
+			$safeHeadline = preg_replace( '//e',
 							    "\$this->mLinkHolders['texts'][\$1]",
-							    $canonized_headline );
-			$canonized_headline = preg_replace( '//e',
+							    $safeHeadline );
+			$safeHeadline = preg_replace( '//e',
 							    "\$this->mInterwikiLinkHolders['texts'][\$1]",
-							    $canonized_headline );
+							    $safeHeadline );
 
 			# Strip out HTML (other than plain  and : bug 8393)
 			$tocline = preg_replace(
 				array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
 				array( '',                          '<$1>'),
-				$canonized_headline
+				$safeHeadline
 			);
 			$tocline = trim( $tocline );
 
 			# For the anchor, strip out HTML-y stuff period
-			$canonized_headline = preg_replace( '/<.*?'.'>/', '', $canonized_headline );
-			$canonized_headline = trim( $canonized_headline );
+			$safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
+			$safeHeadline = trim( $safeHeadline );
 
 			# Save headline for section edit hint before it's escaped
-			$headline_hint = $canonized_headline;
-			$canonized_headline = Sanitizer::escapeId( $canonized_headline );
-			$refers[$headlineCount] = $canonized_headline;
+			$headlineHint = $safeHeadline;
+			$safeHeadline = Sanitizer::escapeId( $safeHeadline );
+			$refers[$headlineCount] = $safeHeadline;
 
 			# 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];
+			isset( $refers[$safeHeadline] ) ? $refers[$safeHeadline]++ : $refers[$safeHeadline] = 1;
+			$refcount[$headlineCount] = $refers[$safeHeadline];
 
 			# Don't number the heading if it is the only one (looks silly)
 			if( $doNumberHeadings && count( $matches[3] ) > 1) {
@@ -3669,29 +3545,33 @@ class Parser
 			}
 
 			# Create the anchor for linking from the TOC to the section
-			$anchor = $canonized_headline;
+			$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  tag
-			if( $showEditLink && ( !$istemplate || $templatetitle !== "" ) ) {
-				if( $istemplate )
-					$editlink = $sk->editSectionLinkForOther($templatetitle, $templatesection);
-				else
-					$editlink = $sk->editSectionLink($this->mTitle, $sectionCount+1, $headline_hint);
+			if( $showEditLink && $sectionIndex !== false ) {
+				if( $isTemplate ) {
+					# Put a T flag in the section identifier, to indicate to extractSections() 
+					# that sections inside  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++;
-			if( !$istemplate )
-				$sectionCount++;
 		}
 
+		$this->mOutput->setSections( $tocraw );
+
 		# Never ever show TOC if no headers
 		if( $numVisible < 1 ) {
 			$enoughToc = false;
@@ -3750,21 +3630,19 @@ class Parser
 	 */
 	function preSaveTransform( $text, &$title, $user, $options, $clearState = true ) {
 		$this->mOptions = $options;
-		$this->mTitle =& $title;
-		$this->setOutputType( OT_WIKI );
+		$this->setTitle( $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 );
+		$text = $this->pstPass2( $text, $user );
+		$text = $this->mStripState->unstripBoth( $text );
 		return $text;
 	}
 
@@ -3772,31 +3650,32 @@ class Parser
 	 * Pre-save transform helper function
 	 * @private
 	 */
-	function pstPass2( $text, &$stripState, $user ) {
+	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 );
-		}
-		$d = $wgContLang->timeanddate( date( 'YmdHis' ), false, false) .
-		  ' (' . date( 'T' ) . ')';
-		if ( isset( $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 );
 
-		# Strip out  etc. added via replaceVariables
-		$text = $this->strip( $text, $stripState, false, array( 'gallery' ) );
-
 		# Signatures
 		$sigText = $this->getUserSig( $user );
 		$text = strtr( $text, array(
@@ -3870,8 +3749,13 @@ class Parser
 		$nickname = $this->cleanSigInSig( $nickname );
 
 		# If we're still here, make it a link to the user page
-		$userpage = $user->getUserPage();
-		return( '[[' . $userpage->getPrefixedText() . '|' . wfEscapeWikiText( $nickname ) . ']]' );
+		$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 );
+		}
 	}
 
 	/**
@@ -3895,18 +3779,30 @@ class Parser
 	 * @return string Signature text
 	 */
 	function cleanSig( $text, $parsing = false ) {
-		global $wgTitle;
-		$this->startExternalParse( $wgTitle, new ParserOptions(), $parsing ? OT_WIKI : OT_MSG );
+		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 );
-		$text = $this->replaceVariables( $text );
+		$dom = $this->preprocessToDom( $text );
+		$frame = $this->getPreprocessor()->newFrame();
+		$text = $frame->expand( $dom );
+
+		if ( !$parsing ) {
+			$text = $this->mStripState->unstripBoth( $text );
+		}
 
-		$this->clearState();
 		return $text;
 	}
 
@@ -3926,7 +3822,7 @@ class Parser
 	 * @public
 	 */
 	function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
-		$this->mTitle =& $title;
+		$this->setTitle( $title );
 		$this->mOptions = $options;
 		$this->setOutputType( $outputType );
 		if ( $clearState ) {
@@ -3935,11 +3831,11 @@ class Parser
 	}
 
 	/**
-	 * Transform a MediaWiki message by replacing magic variables.
+	 * Wrapper for preprocess()
 	 *
-	 * @param string $text the text to transform
+	 * @param string $text the text to preprocess
 	 * @param ParserOptions $options  options
-	 * @return string the text with variables substituted
+	 * @return string
 	 * @public
 	 */
 	function transformMsg( $text, $options ) {
@@ -3955,16 +3851,7 @@ class Parser
 		$executing = true;
 
 		wfProfileIn($fname);
-
-		if ( $wgTitle && !( $wgTitle instanceof FakeTitle ) ) {
-			$this->mTitle = $wgTitle;
-		} else {
-			$this->mTitle = Title::newFromText('msg');
-		}
-		$this->mOptions = $options;
-		$this->setOutputType( OT_MSG );
-		$this->clearState();
-		$text = $this->replaceVariables( $text );
+		$text = $this->preprocess( $text, $wgTitle, $options );
 
 		$executing = false;
 		wfProfileOut($fname);
@@ -3990,6 +3877,7 @@ class Parser
 		$tag = strtolower( $tag );
 		$oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
 		$this->mTagHooks[$tag] = $callback;
+		$this->mStripList[] = $tag;
 
 		return $oldVal;
 	}
@@ -4002,6 +3890,14 @@ class Parser
 		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:
@@ -4013,8 +3909,6 @@ class Parser
 	 *   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
@@ -4027,8 +3921,8 @@ class Parser
 	 * @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;
+		$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
+		$this->mFunctionHooks[$id] = array( $callback, $flags );
 
 		# Add to function cache
 		$mw = MagicWord::get( $id );
@@ -4068,10 +3962,7 @@ class Parser
 	/**
 	 * Replace  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
+	 * 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 ) {
@@ -4083,6 +3974,7 @@ class Parser
 
 		$pdbks = array();
 		$colours = array();
+		$linkcolour_ids = array();
 		$sk = $this->mOptions->getSkin();
 		$linkCache =& LinkCache::singleton();
 
@@ -4111,21 +4003,21 @@ class Parser
 
 				# Check if it's a static known link, e.g. interwiki
 				if ( $title->isAlwaysKnown() ) {
-					$colours[$pdbk] = 1;
+					$colours[$pdbk] = '';
 				} elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-					$colours[$pdbk] = 1;
+					$colours[$pdbk] = '';
 					$this->mOutput->addLink( $title, $id );
 				} elseif ( $linkCache->isBadLink( $pdbk ) ) {
-					$colours[$pdbk] = 0;
+					$colours[$pdbk] = 'new';
 				} elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
-					$colours[$pdbk] = 0;
+					$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";
+						$query =  "SELECT page_id, page_namespace, page_title, page_is_redirect";
 						if ( $threshold > 0 ) {
-							$query .= ', page_len, page_is_redirect';
+							$query .= ', page_len';
 						}
 						$query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
 					} elseif ( $current != $ns ) {
@@ -4148,20 +4040,17 @@ class Parser
 
 				# 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 );
 					$this->mOutput->addLink( $title, $s->page_id );
-
-					$colours[$pdbk] = ( $threshold == 0 || (
-								$s->page_len >= $threshold || # always true if $threshold <= 0
-							        $s->page_is_redirect ||
-							        !Namespace::isContent( $s->page_namespace ) )
-							    ? 1 : 2 );
+					$colours[$pdbk] = $sk->getLinkColour( $s, $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' );
 
@@ -4217,9 +4106,9 @@ class Parser
 					// construct query
 					$titleClause = $linkBatch->constructSet('page', $dbr);
 
-					$variantQuery =  "SELECT page_id, page_namespace, page_title";
+					$variantQuery =  "SELECT page_id, page_namespace, page_title, page_is_redirect";
 					if ( $threshold > 0 ) {
-						$variantQuery .= ', page_len, page_is_redirect';
+						$variantQuery .= ', page_len';
 					}
 
 					$variantQuery .= " FROM $page WHERE $titleClause";
@@ -4257,18 +4146,10 @@ class Parser
 
 								// 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;
-								}
+								$colours[$varPdbk] = $sk->getLinkColour( $s, $threshold );
+								$linkcolour_ids[$s->page_id] = $pdbk;
 							}
+							wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
 						}
 
 						// check if the object is a variant of a category
@@ -4301,19 +4182,15 @@ class Parser
 				$pdbk = $pdbks[$key];
 				$searchkey = "";
 				$title = $this->mLinkHolders['titles'][$key];
-				if ( empty( $colours[$pdbk] ) ) {
+				if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
 					$linkCache->addBadLinkObj( $title );
-					$colours[$pdbk] = 0;
+					$colours[$pdbk] = 'new';
 					$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,
+				} else {
+					$replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
 									$this->mLinkHolders['texts'][$key],
 									$this->mLinkHolders['queries'][$key] );
 				}
@@ -4466,13 +4343,7 @@ class Parser
 				$label = '';
 			}
 
-			$pout = $this->parse( $label,
-				$this->mTitle,
-				$this->mOptions,
-				false, // Strip whitespace...?
-				false  // Don't clear state!
-			);
-			$html = $pout->getText();
+			$html = $this->recursiveTagParse( trim( $label ) );
 
 			$ig->add( $nt, $html );
 
@@ -4647,12 +4518,12 @@ class Parser
 	 * 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
+	 * @param PPFrame $frame
 	 * @return string
 	 * @private
 	 */
-	function attributeStripCallback( &$text, $args ) {
-		$text = $this->replaceVariables( $text, $args );
+	function attributeStripCallback( &$text, $frame = false ) {
+		$text = $this->replaceVariables( $text, $frame );
 		$text = $this->mStripState->unstripBoth( $text );
 		return $text;
 	}
@@ -4680,123 +4551,112 @@ class Parser
 	 *
 	 * 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.
+	 * @param string $text Page wikitext
+	 * @param string $section A section identifier string of the form:
+	 *    -  - ... - 
+ * + * 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 (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='' ) { - # I.... _hope_ this is right. - # Otherwise, sometimes we don't have things initialized properly. + private function extractSections( $text, $section, $mode, $newText='' ) { + global $wgTitle; $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( 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\\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; + $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; } } - $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 + // Preprocess the text + $root = $this->preprocessToDom( $text, $flags ); + + // 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; } } - } 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 ( $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($rv)) - # reinsert stripped tags - $rv = trim( $stripState->unstripBoth( $rv ) ); - return $rv; + if ( is_string( $outText ) ) { + // Re-insert stripped tags + $outText = trim( $this->mStripState->unstripBoth( $outText ) ); + } + + return $outText; } /** @@ -4806,9 +4666,9 @@ class Parser * * 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 + * @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='' ) { @@ -4874,21 +4734,120 @@ class Parser : $this->mTitle->getPrefixedText(); } } -} -/** - * @todo document, briefly. - * @addtogroup Parser - */ -class OnlyIncludeReplacer { - var $output = ''; + /** + * 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 ); + } - function replace( $matches ) { - if ( substr( $matches[1], -1 ) == "\n" ) { - $this->output .= substr( $matches[1], 0, -1 ); - } else { - $this->output .= $matches[1]; + /** + * 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, $this->mMarkerSuffix, $markerStart ); + if ( $markerEnd === false ) { + $out .= substr( $s, $markerStart ); + break; + } else { + $markerEnd += strlen( $this->mMarkerSuffix ); + $out .= substr( $s, $markerStart, $markerEnd - $markerStart ); + $i = $markerEnd; + } + } + } + return $out; } } @@ -4906,23 +4865,49 @@ class StripState { function unstripGeneral( $text ) { wfProfileIn( __METHOD__ ); - $text = $this->general->replace( $text ); + do { + $oldText = $text; + $text = $this->general->replace( $text ); + } while ( $text != $oldText ); wfProfileOut( __METHOD__ ); return $text; } function unstripNoWiki( $text ) { wfProfileIn( __METHOD__ ); - $text = $this->nowiki->replace( $text ); + do { + $oldText = $text; + $text = $this->nowiki->replace( $text ); + } while ( $text != $oldText ); wfProfileOut( __METHOD__ ); return $text; } function unstripBoth( $text ) { wfProfileIn( __METHOD__ ); - $text = $this->general->replace( $text ); - $text = $this->nowiki->replace( $text ); + do { + $oldText = $text; + $text = $this->general->replace( $text ); + $text = $this->nowiki->replace( $text ); + } while ( $text != $oldText ); wfProfileOut( __METHOD__ ); return $text; } } + +/** + * @todo document, briefly. + * @addtogroup 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/ParserOptions.php b/includes/ParserOptions.php index 2200bfea..996bba21 100644 --- a/includes/ParserOptions.php +++ b/includes/ParserOptions.php @@ -21,7 +21,12 @@ class ParserOptions var $mTidy; # Ask for tidy cleanup var $mInterfaceMessage; # Which lang to call for PLURAL and GRAMMAR var $mMaxIncludeSize; # Maximum size of template expansions, in bytes + var $mMaxPPNodeCount; # Maximum number of nodes touched by 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 @@ -36,7 +41,11 @@ class ParserOptions function getTidy() { return $this->mTidy; } function getInterfaceMessage() { return $this->mInterfaceMessage; } 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 ) ) { @@ -52,6 +61,13 @@ class ParserOptions 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 ); } @@ -65,7 +81,12 @@ class ParserOptions function setSkin( $x ) { $this->mSkin = $x; } function setInterfaceMessage( $x ) { return wfSetVar( $this->mInterfaceMessage, $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 ); @@ -83,6 +104,7 @@ class ParserOptions function initialiseFromUser( $userInput ) { global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages; global $wgAllowExternalImagesFrom, $wgAllowSpecialInclusion, $wgMaxArticleSize; + global $wgMaxPPNodeCount, $wgMaxTemplateDepth; $fname = 'ParserOptions::initialiseFromUser'; wfProfileIn( $fname ); if ( !$userInput ) { @@ -111,7 +133,11 @@ class ParserOptions $this->mTidy = false; $this->mInterfaceMessage = false; $this->mMaxIncludeSize = $wgMaxArticleSize * 1024; + $this->mMaxPPNodeCount = $wgMaxPPNodeCount; + $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 index d4daf1d1..9b3c12c1 100644 --- a/includes/ParserOutput.php +++ b/includes/ParserOutput.php @@ -20,7 +20,9 @@ class ParserOutput $mNewSection, # Show a new section link? $mNoGallery, # No gallery on category page? (__NOGALLERY__) $mHeadItems, # Items to put in the section - $mOutputHooks; # Hook tags as per $wgParserOutputHooks + $mOutputHooks, # Hook tags as per $wgParserOutputHooks + $mWarnings, # Warning text to be returned to the user. Wikitext formatted. + $mSections; # Table of contents /** * Overridden title for display @@ -37,6 +39,7 @@ class ParserOutput $this->mCacheTime = ''; $this->mVersion = Parser::VERSION; $this->mTitleText = $titletext; + $this->mSections = array(); $this->mLinks = array(); $this->mTemplates = array(); $this->mImages = array(); @@ -46,6 +49,7 @@ class ParserOutput $this->mHeadItems = array(); $this->mTemplateIds = array(); $this->mOutputHooks = array(); + $this->mWarnings = array(); } function getText() { return $this->mText; } @@ -54,6 +58,7 @@ class ParserOutput 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; } @@ -61,6 +66,7 @@ class ParserOutput function getNoGallery() { return $this->mNoGallery; } function getSubtitle() { return $this->mSubtitle; } function getOutputHooks() { return (array)$this->mOutputHooks; } + function getWarnings() { return isset( $this->mWarnings ) ? $this->mWarnings : array(); } function containsOldMagic() { return $this->mContainsOldMagic; } function setText( $text ) { return wfSetVar( $this->mText, $text ); } @@ -68,11 +74,13 @@ class ParserOutput 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 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; } function addOutputHook( $hook, $data = false ) { $this->mOutputHooks[] = array( $hook, $data ); @@ -165,6 +173,17 @@ class ParserOutput 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] ); + } + } diff --git a/includes/Parser_DiffTest.php b/includes/Parser_DiffTest.php new file mode 100644 index 00000000..d88709f0 --- /dev/null +++ b/includes/Parser_DiffTest.php @@ -0,0 +1,85 @@ +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 new file mode 100644 index 00000000..c10de257 --- /dev/null +++ b/includes/Parser_OldPP.php @@ -0,0 +1,4942 @@ +"\\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 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 \\2', + # french spaces, Guillemet-right + '/(\\302\\253) /' => '\\1 ', + ); + $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 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''] --> + # Somethingcool> + '/(<([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>/' => + '\\1\\2\\3\\1\\4', + # 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>)([^<]*)(<\/\\2>)/' => + '\\1\\3<div\\5>\\6</div>\\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"; + } + $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 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' ), + * 'tag content' ) ) + * + * @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, + $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 + * 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 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 = ''. +'test'.$text.''; + if( $wgTidyInternal ) { + $correctedtext = self::internalTidy( $wrappedtext ); + } else { + $correctedtext = self::externalTidy( $wrappedtext ); + } + if( is_null( $correctedtext ) ) { + wfDebug( "Tidy error detected!\n" ); + return $text . "\n\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 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( '
' , $indent_level ) . ""; + 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 = '' . substr ( $line , 2 ); + $last_tag = array_pop ( $last_tag_history ); + + if ( !array_pop ( $has_opened_tr ) ) { + $line = "{$line}"; + } + + if ( array_pop ( $tr_history ) ) { + $line = "{$line}"; + } + + if ( array_pop ( $td_history ) ) { + $line = "{$line}"; + } + array_pop ( $tr_attributes ); + $lines[$key] = $line . str_repeat( '
' , $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 = ''; + } + + if ( array_pop ( $td_history ) ) { + $line = "{$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 = "\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 = "{$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[] = '' ; + } + if ( array_pop ( $tr_history ) ) { + $lines[] = '' ; + } + if ( !array_pop ( $has_opened_tr ) ) { + $lines[] = "" ; + } + + $lines[] = '' ; + } + + $output = implode ( "\n" , $lines ) ; + + // special case: don't return empty table + if( $output == "\n\n
" ) { + $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 tags and sections + $text = strtr( $text, array( '' => '' , '' => '' ) ); + $text = strtr( $text, array( '' => '', '' => '') ); + $text = StringUtils::delimiterReplace( '', '', '', $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
', $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 + | # 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 = 'ISBN $isbn"; + } 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 = "{$keyword} {$id}"; + } + 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", + "\\1\\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 .= ''; $state = ''; } + else if ($state == 'bi') + { $output .= ''; $state = 'b'; } + else if ($state == 'ib') + { $output .= ''; $state = 'b'; } + else if ($state == 'both') + { $output .= ''.$buffer.''; $state = 'b'; } + else # $state can be 'b' or '' + { $output .= ''; $state .= 'i'; } + } + else if (strlen ($r) == 3) + { + if ($state == 'b') + { $output .= ''; $state = ''; } + else if ($state == 'bi') + { $output .= ''; $state = 'i'; } + else if ($state == 'ib') + { $output .= ''; $state = 'i'; } + else if ($state == 'both') + { $output .= ''.$buffer.''; $state = 'i'; } + else # $state can be 'i' or '' + { $output .= ''; $state .= 'b'; } + } + else if (strlen ($r) == 5) + { + if ($state == 'b') + { $output .= ''; $state = 'i'; } + else if ($state == 'i') + { $output .= ''; $state = 'b'; } + else if ($state == 'bi') + { $output .= ''; $state = ''; } + else if ($state == 'ib') + { $output .= ''; $state = ''; } + else if ($state == 'both') + { $output .= ''.$buffer.''; $state = ''; } + else # ($state == '') + { $buffer = ''; $state = 'both'; } + } + } + $i++; + } + # Now close all remaining tags. Notice that the order is important. + if ($state == 'b' || $state == 'ib') + $output .= ''; + if ($state == 'i' || $state == 'bi' || $state == 'ib') + $output .= ''; + if ($state == 'bi') + $output .= ''; + # There might be lonely ''''', so make sure we have a buffer + if ($state == 'both' && $buffer) + $output .= ''.$buffer.''; + 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' (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 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 ö 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('<', '>'), 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 ) { + $link = $sk->makeMediaLinkObj( $nt, $text ); + # 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 = "{$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 = '{$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 = '{$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 = '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 .= '
  • '; } + else if ( '#' == $char ) { $result .= '
    1. '; } + else if ( ':' == $char ) { $result .= '
      '; } + else if ( ';' == $char ) { + $result .= '
      '; + $this->mDTopen = true; + } + else { $result = ''; } + + return $result; + } + + /* private */ function nextItem( $char ) { + if ( '*' == $char || '#' == $char ) { return '
    2. '; } + else if ( ':' == $char || ';' == $char ) { + $close = ''; + if ( $this->mDTopen ) { $close = ''; } + if ( ';' == $char ) { + $this->mDTopen = true; + return $close . '
      '; + } else { + $this->mDTopen = false; + return $close . '
      '; + } + } + return ''; + } + + /* private */ function closeList( $char ) { + if ( '*' == $char ) { $text = '
'; } + else if ( '#' == $char ) { $text = ''; } + else if ( ':' == $char ) { + if ( $this->mDTopen ) { + $this->mDTopen = false; + $text = ''; + } else { + $text = ''; + } + } + else { return ''; } + 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('/
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('/(?: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().'
';
+							$this->mLastSection = 'pre';
+						}
+						$t = substr( $t, 1 );
+					} else {
+						// paragraph
+						if ( '' == trim($t) ) {
+							if ( $paragraphStack ) {
+								$output .= $paragraphStack.'
'; + $paragraphStack = false; + $this->mLastSection = 'p'; + } else { + if ($this->mLastSection != 'p' ) { + $output .= $this->closeParagraph(); + $this->mLastSection = ''; + $paragraphStack = '

'; + } else { + $paragraphStack = '

'; + } + } + } else { + if ( $paragraphStack ) { + $output .= $paragraphStack; + $paragraphStack = false; + $this->mLastSection = 'p'; + } else if ($this->mLastSection != 'p') { + $output .= $this->closeParagraph().'

'; + $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 .= '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 tag or an 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 + 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 + 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 + $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]]"; + 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]]"; + $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 tags, only include them + if ( in_string( '', $text ) && in_string( '', $text ) ) { + $replacer = new OnlyIncludeReplacer; + StringUtils::delimiterReplaceCallback( '', '', + array( &$replacer, 'replace' ), $text ); + $text = $replacer->output; + } + # Remove sections and tags + $text = StringUtils::delimiterReplace( '', '', '', $text ); + $text = strtr( $text, array( '' => '' , '' => '' ) ); + + if( $this->ot['html'] || $this->ot['pre'] ) { + # Strip ,

, 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]]";
+			$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, "" . $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 );
+		$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 ) {
+		$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( false, &$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'] .
+				'';
+		}
+
+		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 
+	 * 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( '', $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( '/[1-6])(?P.*?'.'>)(?P
.*?)<\/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("//", $headline, $mat)) { + $istemplate = 1; + $templatetitle = base64_decode($mat[1]); + $templatesection = 1 + (int)base64_decode($mat[2]); + $headline = preg_replace("//", "", $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 by expanding the relevant sections + $canonized_headline = $this->mStripState->unstripBoth( $headline ); + + # Remove link placeholders by the link text. + # + # turns into + # link text with suffix + $canonized_headline = preg_replace( '//e', + "\$this->mLinkHolders['texts'][\$1]", + $canonized_headline ); + $canonized_headline = preg_replace( '//e', + "\$this->mInterwikiLinkHolders['texts'][\$1]", + $canonized_headline ); + + # Strip out HTML (other than plain and : 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 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]>/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( '', $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 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. special text + * 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 + * @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 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"; + if ( $threshold > 0 ) { + $query .= ', 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 ); + $this->mOutput->addLink( $title, $s->page_id ); + + $colours[$pdbk] = ( $threshold == 0 || ( + $s->page_len >= $threshold || # always true if $threshold <= 0 + $s->page_is_redirect || + !Namespace::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"; + if ( $threshold > 0 ) { + $variantQuery .= ', 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 ); + $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 = ""; + $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( + '/()/', + $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( + '//', + $replacer->cb(), + $text ); + wfProfileOut( $fname.'-interwiki' ); + } + + wfProfileOut( $fname ); + return $colours; + } + + /** + * Replace 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( + '//', + 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( '', '', '$1', $text, 'i' ); + + $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); + return wfOpenElement( 'pre', $attribs ) . + Xml::escapeTagsOnly( $content ) . + '
'; + } + + /** + * 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\\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 \Heading\, 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( '//', '', $text ); + $text = $this->mStripState->unstripBoth( $text ); + return $text; + } +} + diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php new file mode 100644 index 00000000..bddfb9f1 --- /dev/null +++ b/includes/PrefixSearch.php @@ -0,0 +1,135 @@ +getInterwiki() == '' ) { + $ns = $title->getNamespace(); + return self::searchBackend( + $title->getNamespace(), $title->getText(), $limit ); + } + + // Is this a namespace prefix? + $title = Title::newFromText( $search . 'Dummy' ); + if( $title && $title->getText() == 'Dummy' + && $title->getNamespace() != NS_MAIN + && $title->getInterwiki() == '' ) { + return self::searchBackend( + $title->getNamespace(), '', $limit ); + } + + return self::searchBackend( 0, $search, $limit ); + } + + + /** + * Do a prefix search of titles and return a list of matching page names. + * @param string $search + * @param int $limit + * @return array of strings + */ + protected static function searchBackend( $ns, $search, $limit ) { + if( $ns == NS_MEDIA ) { + $ns = NS_IMAGE; + } elseif( $ns == NS_SPECIAL ) { + return self::specialSearch( $search, $limit ); + } + + $srchres = array(); + if( wfRunHooks( 'PrefixSearchBackend', array( $ns, $search, $limit, &$srchres ) ) ) { + return self::defaultSearchBackend( $ns, $search, $limit ); + } + return $srchres; + } + + /** + * Prefix search special-case for Special: namespace. + */ + protected static function specialSearch( $search, $limit ) { + global $wgContLang; + $searchKey = $wgContLang->caseFold( $search ); + + // Unlike SpecialPage itself, we want the canonical forms of both + // canonical and alias title forms... + SpecialPage::initList(); + SpecialPage::initAliasList(); + $keys = array(); + foreach( array_keys( SpecialPage::$mList ) as $page ) { + $keys[$wgContLang->caseFold( $page )] = $page; + } + foreach( $wgContLang->getSpecialPageAliases() as $page => $aliases ) { + foreach( $aliases as $alias ) { + $keys[$wgContLang->caseFold( $alias )] = $alias; + } + } + ksort( $keys ); + + $srchres = array(); + foreach( $keys as $pageKey => $page ) { + if( $searchKey === '' || strpos( $pageKey, $searchKey ) === 0 ) { + $srchres[] = Title::makeTitle( NS_SPECIAL, $page )->getPrefixedText(); + } + if( count( $srchres ) >= $limit ) { + break; + } + } + return $srchres; + } + + /** + * Unless overridden by PrefixSearchBackend hook... + * This is case-sensitive except the first letter (per $wgCapitalLinks) + * + * @param int $ns Namespace to search in + * @param string $search term + * @param int $limit max number of items to return + * @return array of title strings + */ + protected static function defaultSearchBackend( $ns, $search, $limit ) { + global $wgCapitalLinks, $wgContLang; + + if( $wgCapitalLinks ) { + $search = $wgContLang->ucfirst( $search ); + } + + // Prepare nested request + $req = new FauxRequest(array ( + 'action' => 'query', + 'list' => 'allpages', + 'apnamespace' => $ns, + 'aplimit' => $limit, + 'apprefix' => $search + )); + + // Execute + $module = new ApiMain($req); + $module->execute(); + + // Get resulting data + $data = $module->getResultData(); + + // Reformat useful data for future printing by JSON engine + $srchres = array (); + foreach ($data['query']['allpages'] as & $pageinfo) { + // Note: this data will no be printable by the xml engine + // because it does not support lists of unnamed items + $srchres[] = $pageinfo['title']; + } + + return $srchres; + } + +} + +?> \ No newline at end of file diff --git a/includes/Preprocessor.php b/includes/Preprocessor.php new file mode 100644 index 00000000..34bc1e5b --- /dev/null +++ b/includes/Preprocessor.php @@ -0,0 +1,154 @@ + node into an associative array containing: + * name PPNode name + * index String index + * value PPNode value + */ + function splitArg(); + + /** + * Split an 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 node + */ + function splitHeading(); +} + diff --git a/includes/Preprocessor_DOM.php b/includes/Preprocessor_DOM.php new file mode 100644 index 00000000..0e2e9a16 --- /dev/null +++ b/includes/Preprocessor_DOM.php @@ -0,0 +1,1356 @@ +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 / 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, '' ) !== false && strpos( $text, '' ) !== 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 = ''; + $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 + $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 + $startPos = strpos( $text, '', $i ); + if ( $startPos === false ) { + // Ignored section runs to the end + $accum .= '' . htmlspecialchars( substr( $text, $i ) ) . ''; + break; + } + $tagEndPos = $startPos + strlen( '' ); // past-the-end + $accum .= '' . htmlspecialchars( 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 .= 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 + if ( $enableOnlyinclude && substr( $text, $i, strlen( '' ) ) == '' ) { + $findOnlyinclude = true; + continue; + } + + // Determine element name + if ( !preg_match( $elementsRegex, $text, $matches, 0, $i + 1 ) ) { + // Element name missing or not listed + $accum .= '<'; + ++$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 .= '' . htmlspecialchars( $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 && 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 .= '' . htmlspecialchars( $inner ) . ''; + } + continue; + } + $name = $matches[1]; + $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 .= '<'; + ++$i; + continue; + } + + // Handle ignored tags + if ( in_array( $name, $ignoredTags ) ) { + $accum .= '' . htmlspecialchars( substr( $text, $i, $tagEndPos - $i + 1 ) ) . ''; + $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 = '' . htmlspecialchars( $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 = ''; + } + } + // and just become tags + if ( in_array( $name, $ignoredElements ) ) { + $accum .= '' . htmlspecialchars( substr( $text, $tagStartPos, $i - $tagStartPos ) ) + . ''; + continue; + } + + $accum .= ''; + if ( $attrEnd <= $attrStart ) { + $attr = ''; + } else { + $attr = substr( $text, $attrStart, $attrEnd - $attrStart ); + } + $accum .= '' . htmlspecialchars( $name ) . '' . + // Note that the attr element contains the whitespace between name and attribute, + // this is necessary for precise reconstruction during pre-save transform. + '' . htmlspecialchars( $attr ) . ''; + if ( $inner !== null ) { + $accum .= '' . htmlspecialchars( $inner ) . ''; + } + $accum .= $close . ''; + } + + 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 + $element = "$accum"; + $headingIndex++; + } else { + // Single equals sign on its own line, count=0 + $element = $accum; + } + } else { + // No match, no , 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"; + $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 .= "$argName=$argValue"; + } else { + $element .= "{$part->out}"; + $argIndex++; + } + } + $element .= ""; + } + + # 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 .= ''; + $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() + */ +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(); + } + } +} + +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; + } +} + +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() + */ +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 ) { + if ( is_string( $root ) ) { + return $root; + } + + if ( ++$this->parser->mPPNodeCount > $this->parser->mOptions->mMaxPPNodeCount ) + { + return 'Node-count limit exceeded'; + } + + 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 etc. + # OT_WIKI will only respect 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 children of + # 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-{$this->parser->mMarkerSuffix}"; + $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--; + } + } + } + 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 + */ +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; + } +} + +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 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 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 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 new file mode 100644 index 00000000..2034278d --- /dev/null +++ b/includes/Preprocessor_Hash.php @@ -0,0 +1,1471 @@ + nodes that aren't at the top are replaced with + */ + +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 / 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 ) { + wfDebug( __METHOD__."\n" . $text . "\n" ); + 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, '' ) !== false && strpos( $text, '' ) !== 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 + $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 + $startPos = strpos( $text, '', $i ); + if ( $startPos === false ) { + // Ignored section runs to the end + $accum->addNodeWithText( 'ignore', substr( $text, $i ) ); + break; + } + $tagEndPos = $startPos + strlen( '' ); // 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 + if ( $enableOnlyinclude && substr( $text, $i, strlen( '' ) ) == '' ) { + $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]; + $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( $name, $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; + } + } + // and just become tags + if ( in_array( $name, $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 + $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 , 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() + */ +class PPDStack_Hash extends PPDStack { + function __construct() { + $this->elementClass = 'PPDStackElement_Hash'; + parent::__construct(); + $this->rootAccum = new PPDAccum_Hash; + } +} + +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; + } +} + +class PPDPart_Hash extends PPDPart { + function __construct( $out = '' ) { + $accum = new PPDAccum_Hash; + if ( $out !== '' ) { + $accum->addLiteral( $out ); + } + parent::__construct( $accum ); + } +} + +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() + */ +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 'Node-count limit exceeded'; + } + + $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 etc. + # OT_WIKI will only respect 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-{$this->parser->mMarkerSuffix}"; + $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--; + } + } + } + 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 + */ +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; + } +} + +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>$innername}>"; + } + } + + 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 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 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 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