From 8f416baead93a48e5799e44b8bd2e2c4859f4e04 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 14 Sep 2007 13:18:58 +0200 Subject: auf Version 1.11 aktualisiert; Login-Bug behoben --- HISTORY | 492 ++- Makefile | 22 +- RELEASE-NOTES | 1025 +++--- StartProfiler.php | 2 +- Test.php | 623 ++-- UPGRADE | 34 +- api.php | 25 +- api.php5 | 1 + config/index.php | 291 +- config/index.php5 | 6 + docs/README | 6 +- docs/design.txt | 25 +- docs/hooks.txt | 332 +- docs/memcached.txt | 2 +- docs/schema.txt | 3 + docs/title.txt | 9 +- extensions/LLAuthPlugin.php | 15 +- img_auth.php | 86 +- img_auth.php5 | 1 + includes/AjaxDispatcher.php | 51 +- includes/AjaxFunctions.php | 34 +- includes/AjaxResponse.php | 32 +- includes/Article.php | 345 +- includes/AuthPlugin.php | 5 +- includes/AutoLoader.php | 50 +- includes/BagOStuff.php | 134 +- includes/Block.php | 23 +- includes/CacheDependency.php | 2 +- includes/CategoryPage.php | 37 +- includes/Categoryfinder.php | 2 +- includes/ChangesList.php | 11 +- includes/CoreParserFunctions.php | 27 +- includes/Credits.php | 2 +- includes/Database.php | 173 +- includes/DatabaseFunctions.php | 2 +- includes/DatabaseOracle.php | 18 +- includes/DatabasePostgres.php | 98 +- includes/DateFormatter.php | 2 +- includes/DefaultSettings.php | 319 +- includes/Defines.php | 58 +- includes/DifferenceEngine.php | 153 +- includes/DjVuImage.php | 2 + includes/EditPage.php | 258 +- includes/EmaillingJob.php | 25 + includes/EnotifNotifyJob.php | 26 + includes/Exception.php | 2 +- includes/Exif.php | 4 +- includes/Export.php | 4 +- includes/ExternalEdit.php | 4 +- includes/ExternalStore.php | 7 +- includes/ExternalStoreDB.php | 2 +- includes/ExternalStoreHttp.php | 2 +- includes/FakeTitle.php | 2 +- includes/FileDeleteForm.php | 220 ++ includes/FileRevertForm.php | 165 + includes/FileStore.php | 8 +- includes/GlobalFunctions.php | 186 +- includes/HTMLCacheUpdate.php | 32 +- includes/HTMLFileCache.php | 2 +- includes/HTMLForm.php | 53 +- includes/HistoryBlob.php | 2 +- includes/Hooks.php | 16 +- includes/HttpFunctions.php | 38 +- includes/IP.php | 47 +- includes/ImageFunctions.php | 111 +- includes/ImageGallery.php | 103 +- includes/ImagePage.php | 544 ++- includes/ImageQueryPage.php | 8 +- includes/JobQueue.php | 112 +- includes/Licenses.php | 2 +- includes/LinkBatch.php | 33 +- includes/LinkCache.php | 2 +- includes/LinkFilter.php | 2 +- includes/Linker.php | 574 +-- includes/LinksUpdate.php | 16 +- includes/LoadBalancer.php | 2 +- includes/LogPage.php | 71 +- includes/MacBinary.php | 4 +- includes/MagicWord.php | 178 +- includes/Math.php | 49 +- includes/MediaTransformOutput.php | 98 +- includes/MemcachedSessions.php | 2 +- includes/MessageCache.php | 33 +- includes/Metadata.php | 2 +- includes/MimeMagic.php | 538 +-- includes/Namespace.php | 88 +- includes/ObjectCache.php | 4 +- includes/OutputHandler.php | 75 +- includes/OutputPage.php | 281 +- includes/PageHistory.php | 26 +- includes/PageQueryPage.php | 2 +- includes/Pager.php | 122 +- includes/Parser.php | 420 ++- includes/ParserCache.php | 2 +- includes/ParserOptions.php | 2 +- includes/ParserOutput.php | 59 +- includes/PatrolLog.php | 18 +- includes/Profiler.php | 5 +- includes/ProfilerSimple.php | 2 +- includes/ProfilerSimpleUDP.php | 2 +- includes/ProfilerStub.php | 2 +- includes/ProtectionForm.php | 90 +- includes/ProxyTools.php | 2 +- includes/QueryPage.php | 18 +- includes/RawPage.php | 2 +- includes/RecentChange.php | 72 +- includes/RefreshLinksJob.php | 48 + includes/Revision.php | 2 +- includes/Sanitizer.php | 92 +- includes/SearchEngine.php | 21 +- includes/SearchMySQL.php | 6 +- includes/SearchMySQL4.php | 2 +- includes/SearchOracle.php | 2 +- includes/SearchPostgres.php | 3 +- includes/SearchTsearch2.php | 2 +- includes/SearchUpdate.php | 2 +- includes/Setup.php | 74 +- includes/SiteConfiguration.php | 2 +- includes/SiteStats.php | 62 +- includes/Skin.php | 104 +- includes/SkinTemplate.php | 60 +- includes/SpecialAllmessages.php | 5 +- includes/SpecialAllpages.php | 112 +- includes/SpecialAncientpages.php | 2 +- includes/SpecialBlockip.php | 69 +- includes/SpecialBlockme.php | 2 +- includes/SpecialBooksources.php | 22 +- includes/SpecialBrokenRedirects.php | 8 +- includes/SpecialCategories.php | 13 +- includes/SpecialConfirmemail.php | 23 +- includes/SpecialContributions.php | 129 +- includes/SpecialDeadendpages.php | 4 +- includes/SpecialDisambiguations.php | 4 +- includes/SpecialDoubleRedirects.php | 9 +- includes/SpecialEmailuser.php | 11 +- includes/SpecialExport.php | 46 +- includes/SpecialFewestrevisions.php | 2 +- includes/SpecialImagelist.php | 20 +- includes/SpecialImport.php | 31 +- includes/SpecialIpblocklist.php | 168 +- includes/SpecialListredirects.php | 2 +- includes/SpecialListusers.php | 28 +- includes/SpecialLockdb.php | 2 +- includes/SpecialLog.php | 106 +- includes/SpecialLonelypages.php | 4 +- includes/SpecialLongpages.php | 2 +- includes/SpecialMIMEsearch.php | 31 +- includes/SpecialMostcategories.php | 3 +- includes/SpecialMostimages.php | 2 +- includes/SpecialMostlinked.php | 2 +- includes/SpecialMostlinkedcategories.php | 2 +- includes/SpecialMostlinkedtemplates.php | 131 + includes/SpecialMostrevisions.php | 2 +- includes/SpecialMovepage.php | 65 +- includes/SpecialNewimages.php | 5 +- includes/SpecialNewpages.php | 22 +- includes/SpecialPage.php | 30 +- includes/SpecialPopularpages.php | 2 +- includes/SpecialPreferences.php | 295 +- includes/SpecialPrefixindex.php | 26 +- includes/SpecialProtectedpages.php | 132 +- includes/SpecialRandompage.php | 2 +- includes/SpecialRandomredirect.php | 2 +- includes/SpecialRecentchanges.php | 16 +- includes/SpecialRecentchangeslinked.php | 3 +- includes/SpecialResetpass.php | 4 +- includes/SpecialRevisiondelete.php | 2 +- includes/SpecialSearch.php | 15 +- includes/SpecialShortpages.php | 2 +- includes/SpecialSpecialpages.php | 6 +- includes/SpecialStatistics.php | 89 +- includes/SpecialUncategorizedcategories.php | 2 +- includes/SpecialUncategorizedimages.php | 2 +- includes/SpecialUncategorizedpages.php | 2 +- includes/SpecialUncategorizedtemplates.php | 31 + includes/SpecialUndelete.php | 146 +- includes/SpecialUnlockdb.php | 2 +- includes/SpecialUnusedcategories.php | 4 +- includes/SpecialUnusedimages.php | 2 +- includes/SpecialUnusedtemplates.php | 5 +- includes/SpecialUnwatchedpages.php | 2 +- includes/SpecialUpload.php | 900 ++--- includes/SpecialUploadMogile.php | 2 +- includes/SpecialUserlogin.php | 34 +- includes/SpecialUserlogout.php | 2 +- includes/SpecialUserrights.php | 225 +- includes/SpecialVersion.php | 17 +- includes/SpecialWantedcategories.php | 2 +- includes/SpecialWantedpages.php | 66 +- includes/SpecialWatchlist.php | 194 +- includes/SpecialWhatlinkshere.php | 35 +- includes/SpecialWithoutinterwiki.php | 10 +- includes/SquidUpdate.php | 21 +- includes/StreamFile.php | 5 +- includes/StringUtils.php | 2 +- includes/StubObject.php | 2 +- includes/Title.php | 338 +- includes/User.php | 284 +- includes/UserMailer.php | 195 +- includes/Utf8Case.php | 2 +- includes/WatchedItem.php | 17 +- includes/WatchlistEditor.php | 493 +++ includes/WebRequest.php | 92 +- includes/WebResponse.php | 2 +- includes/WebStart.php | 2 +- includes/Wiki.php | 39 +- includes/WikiError.php | 2 +- includes/Xml.php | 180 +- includes/XmlFunctions.php | 5 +- includes/ZhClient.php | 1 - includes/ZhConversion.php | 1 - includes/api/ApiBase.php | 165 +- includes/api/ApiFeedWatchlist.php | 121 +- includes/api/ApiFormatBase.php | 41 +- includes/api/ApiFormatJson.php | 31 +- includes/api/ApiFormatJson_json.php | 2 +- includes/api/ApiFormatPhp.php | 6 +- includes/api/ApiFormatWddx.php | 6 +- includes/api/ApiFormatXml.php | 6 +- includes/api/ApiFormatYaml.php | 6 +- includes/api/ApiFormatYaml_spyc.php | 2 +- includes/api/ApiHelp.php | 8 +- includes/api/ApiLogin.php | 139 +- includes/api/ApiMain.php | 268 +- includes/api/ApiOpenSearch.php | 14 +- includes/api/ApiPageSet.php | 115 +- includes/api/ApiQuery.php | 253 +- includes/api/ApiQueryAllLinks.php | 179 + includes/api/ApiQueryAllUsers.php | 204 ++ includes/api/ApiQueryAllpages.php | 104 +- includes/api/ApiQueryBacklinks.php | 125 +- includes/api/ApiQueryBase.php | 247 +- includes/api/ApiQueryCategories.php | 157 + includes/api/ApiQueryCategoryMembers.php | 238 ++ includes/api/ApiQueryExtLinksUsage.php | 200 ++ includes/api/ApiQueryExternalLinks.php | 93 + includes/api/ApiQueryImageInfo.php | 156 + includes/api/ApiQueryImages.php | 118 + includes/api/ApiQueryInfo.php | 126 +- includes/api/ApiQueryLangLinks.php | 94 + includes/api/ApiQueryLinks.php | 162 + includes/api/ApiQueryLogEvents.php | 155 +- includes/api/ApiQueryRecentChanges.php | 117 +- includes/api/ApiQueryRevisions.php | 187 +- includes/api/ApiQuerySearch.php | 151 + includes/api/ApiQuerySiteinfo.php | 202 +- includes/api/ApiQueryUserContributions.php | 231 +- includes/api/ApiQueryUserInfo.php | 133 + includes/api/ApiQueryWatchlist.php | 133 +- includes/api/ApiResult.php | 42 +- includes/cbt/CBTCompiler.php | 2 +- includes/cbt/CBTProcessor.php | 2 +- includes/filerepo/ArchivedFile.php | 108 + includes/filerepo/FSRepo.php | 530 +++ includes/filerepo/File.php | 1133 ++++++ includes/filerepo/FileRepo.php | 404 +++ includes/filerepo/FileRepoStatus.php | 171 + includes/filerepo/ForeignDBFile.php | 42 + includes/filerepo/ForeignDBRepo.php | 57 + includes/filerepo/ICRepo.php | 313 ++ includes/filerepo/LocalFile.php | 1573 +++++++++ includes/filerepo/LocalRepo.php | 65 + includes/filerepo/OldLocalFile.php | 232 ++ includes/filerepo/README | 41 + includes/filerepo/RepoGroup.php | 150 + includes/filerepo/UnregisteredLocalFile.php | 109 + includes/media/BMP.php | 10 +- includes/media/Bitmap.php | 91 +- includes/media/DjVu.php | 16 +- includes/media/Generic.php | 179 +- includes/media/SVG.php | 21 +- includes/memcached-client.php | 40 +- includes/normal/CleanUpTest.php | 2 +- includes/normal/RandomTest.php | 2 +- includes/normal/Utf8Test.php | 2 +- includes/normal/UtfNormal.php | 55 +- includes/normal/UtfNormalBench.php | 2 +- includes/normal/UtfNormalGenerate.php | 2 +- includes/normal/UtfNormalTest.php | 2 +- includes/normal/UtfNormalUtil.php | 2 +- includes/proxy_check.php | 2 +- includes/templates/NoLocalSettings.php | 5 +- includes/templates/Userlogin.php | 49 +- index.php | 2 +- index.php5 | 1 + install-utils.inc | 19 +- languages/Language.php | 159 +- languages/LanguageConverter.php | 4 +- languages/Names.php | 29 +- languages/classes/LanguageAr.php | 1 - languages/classes/LanguageAz.php | 2 +- languages/classes/LanguageBe.php | 2 +- languages/classes/LanguageBe_tarask.php | 88 + languages/classes/LanguageBg.php | 2 +- languages/classes/LanguageBs.php | 2 +- languages/classes/LanguageCs.php | 2 +- languages/classes/LanguageCu.php | 2 +- languages/classes/LanguageEo.php | 2 +- languages/classes/LanguageEt.php | 2 +- languages/classes/LanguageFi.php | 13 +- languages/classes/LanguageFr.php | 2 +- languages/classes/LanguageGa.php | 2 +- languages/classes/LanguageGsw.php | 2 +- languages/classes/LanguageHe.php | 2 +- languages/classes/LanguageHr.php | 2 +- languages/classes/LanguageHu.php | 2 +- languages/classes/LanguageHy.php | 2 +- languages/classes/LanguageJa.php | 2 +- languages/classes/LanguageKk.deps.php | 2 +- languages/classes/LanguageKk.php | 25 +- languages/classes/LanguageKk_kz.php | 2 +- languages/classes/LanguageKo.php | 2 +- languages/classes/LanguageKsh.php | 2 +- languages/classes/LanguageKu.deps.php | 12 + languages/classes/LanguageKu.php | 240 ++ languages/classes/LanguageKu_ku.php | 35 + languages/classes/LanguageLa.php | 2 +- languages/classes/LanguageLt.php | 2 +- languages/classes/LanguageLv.php | 2 +- languages/classes/LanguagePt_br.php | 2 +- languages/classes/LanguageRmy.php | 2 +- languages/classes/LanguageRu.php | 2 +- languages/classes/LanguageSk.php | 2 +- languages/classes/LanguageSl.php | 2 +- languages/classes/LanguageSr.deps.php | 2 +- languages/classes/LanguageSr.php | 2 +- languages/classes/LanguageSr_ec.php | 2 +- languages/classes/LanguageSr_el.deps.php | 2 +- languages/classes/LanguageSr_el.php | 2 +- languages/classes/LanguageTr.php | 2 +- languages/classes/LanguageTyv.php | 2 +- languages/classes/LanguageUk.php | 2 +- languages/classes/LanguageWa.php | 2 +- languages/classes/LanguageZh.deps.php | 2 +- languages/classes/LanguageZh.php | 4 +- languages/classes/LanguageZh_cn.php | 2 +- languages/classes/LanguageZh_yue.php | 2 +- languages/messages/MessagesAb.php | 2 +- languages/messages/MessagesAf.php | 1452 +++++--- languages/messages/MessagesAn.php | 2 +- languages/messages/MessagesAr.php | 1760 ++++++---- languages/messages/MessagesArc.php | 2 +- languages/messages/MessagesAs.php | 22 +- languages/messages/MessagesAst.php | 2 +- languages/messages/MessagesAv.php | 1 - languages/messages/MessagesAy.php | 2 +- languages/messages/MessagesAz.php | 13 +- languages/messages/MessagesBa.php | 4 +- languages/messages/MessagesBar.php | 2 +- languages/messages/MessagesBat_smg.php | 2 +- languages/messages/MessagesBcl.php | 913 +++++ languages/messages/MessagesBe.php | 257 +- languages/messages/MessagesBe_tarask.php | 1112 ++++++ languages/messages/MessagesBg.php | 958 +++-- languages/messages/MessagesBh.php | 22 +- languages/messages/MessagesBm.php | 2 +- languages/messages/MessagesBn.php | 2 +- languages/messages/MessagesBo.php | 22 +- languages/messages/MessagesBpy.php | 54 +- languages/messages/MessagesBr.php | 280 +- languages/messages/MessagesBs.php | 1961 ++++++----- languages/messages/MessagesCa.php | 199 +- languages/messages/MessagesCe.php | 2 +- languages/messages/MessagesCs.php | 259 +- languages/messages/MessagesCsb.php | 640 ++-- languages/messages/MessagesCu.php | 180 +- languages/messages/MessagesCv.php | 7 - languages/messages/MessagesCy.php | 1231 +++---- languages/messages/MessagesDa.php | 3424 +++++++++++------- languages/messages/MessagesDe.php | 1123 +++--- languages/messages/MessagesDv.php | 2 +- languages/messages/MessagesDz.php | 22 +- languages/messages/MessagesEl.php | 3151 +++++++++-------- languages/messages/MessagesEn.php | 3678 ++++++++++---------- languages/messages/MessagesEnRTL.php | 2 +- languages/messages/MessagesEo.php | 783 +++-- languages/messages/MessagesEs.php | 3637 ++++++++++--------- languages/messages/MessagesEt.php | 1756 ++++++---- languages/messages/MessagesEu.php | 2754 ++++++--------- languages/messages/MessagesExt.php | 839 +++++ languages/messages/MessagesFa.php | 3036 +++++++++++----- languages/messages/MessagesFi.php | 448 +-- languages/messages/MessagesFiu_vro.php | 245 +- languages/messages/MessagesFo.php | 6 +- languages/messages/MessagesFr.php | 788 +++-- languages/messages/MessagesFrc.php | 583 ++++ languages/messages/MessagesFrp.php | 2206 ++++++++++++ languages/messages/MessagesFur.php | 1400 ++++---- languages/messages/MessagesFy.php | 1186 +++---- languages/messages/MessagesGa.php | 2381 ++++++------- languages/messages/MessagesGl.php | 797 +++++ languages/messages/MessagesGn.php | 2 +- languages/messages/MessagesGsw.php | 1486 ++++---- languages/messages/MessagesGu.php | 22 +- languages/messages/MessagesHak.php | 1637 +++++++++ languages/messages/MessagesHe.php | 609 ++-- languages/messages/MessagesHi.php | 22 +- languages/messages/MessagesHr.php | 2700 +++++++------- languages/messages/MessagesHsb.php | 902 +++-- languages/messages/MessagesHt.php | 84 + languages/messages/MessagesHu.php | 38 +- languages/messages/MessagesHy.php | 88 +- languages/messages/MessagesIa.php | 1102 +++--- languages/messages/MessagesId.php | 680 ++-- languages/messages/MessagesIi.php | 2 +- languages/messages/MessagesIs.php | 1972 +++++++---- languages/messages/MessagesIt.php | 631 ++-- languages/messages/MessagesJa.php | 387 +- languages/messages/MessagesJbo.php | 3 +- languages/messages/MessagesJv.php | 8 +- languages/messages/MessagesKa.php | 1209 +++---- languages/messages/MessagesKaa.php | 2 +- languages/messages/MessagesKab.php | 1084 +++--- languages/messages/MessagesKg.php | 2 +- languages/messages/MessagesKk.php | 2 +- languages/messages/MessagesKk_cn.php | 1150 +++--- languages/messages/MessagesKk_kz.php | 1130 +++--- languages/messages/MessagesKk_tr.php | 1179 ++++--- languages/messages/MessagesKm.php | 22 +- languages/messages/MessagesKn.php | 643 ++-- languages/messages/MessagesKo.php | 496 +-- languages/messages/MessagesKrj.php | 162 + languages/messages/MessagesKs.php | 22 +- languages/messages/MessagesKsh.php | 2687 +++++++------- languages/messages/MessagesKu.php | 736 +--- languages/messages/MessagesKu_arab.php | 34 + languages/messages/MessagesKu_latn.php | 913 +++++ languages/messages/MessagesKv.php | 2 +- languages/messages/MessagesLa.php | 146 +- languages/messages/MessagesLg.php | 5 +- languages/messages/MessagesLi.php | 92 +- languages/messages/MessagesLn.php | 2 - languages/messages/MessagesLo.php | 692 +++- languages/messages/MessagesLt.php | 581 ++-- languages/messages/MessagesLv.php | 177 +- languages/messages/MessagesMi.php | 2 +- languages/messages/MessagesMk.php | 3076 ++++++++-------- languages/messages/MessagesMl.php | 79 +- languages/messages/MessagesMn.php | 9 + languages/messages/MessagesMr.php | 23 +- languages/messages/MessagesMs.php | 1400 ++++---- languages/messages/MessagesMt.php | 2 +- languages/messages/MessagesMy.php | 205 ++ languages/messages/MessagesMzn.php | 2 +- languages/messages/MessagesNah.php | 2 +- languages/messages/MessagesNap.php | 2 +- languages/messages/MessagesNds.php | 997 +++--- languages/messages/MessagesNds_nl.php | 3 +- languages/messages/MessagesNe.php | 22 +- languages/messages/MessagesNew.php | 22 +- languages/messages/MessagesNl.php | 1127 +++--- languages/messages/MessagesNn.php | 2284 ++++++------ languages/messages/MessagesNo.php | 3177 ++++++++++------- languages/messages/MessagesNon.php | 2 +- languages/messages/MessagesNv.php | 3 +- languages/messages/MessagesOc.php | 280 +- languages/messages/MessagesOr.php | 22 +- languages/messages/MessagesOs.php | 403 ++- languages/messages/MessagesPa.php | 1302 +++++-- languages/messages/MessagesPi.php | 22 +- languages/messages/MessagesPl.php | 3161 ++++++++--------- languages/messages/MessagesPms.php | 874 +++-- languages/messages/MessagesPs.php | 2 +- languages/messages/MessagesPt.php | 3549 ++++++++++--------- languages/messages/MessagesPt_br.php | 1208 +++---- languages/messages/MessagesQu.php | 2 +- languages/messages/MessagesRmy.php | 309 +- languages/messages/MessagesRo.php | 478 +-- languages/messages/MessagesRoa_rup.php | 214 ++ languages/messages/MessagesRu.php | 625 ++-- languages/messages/MessagesSa.php | 24 +- languages/messages/MessagesSah.php | 1348 +++++++ languages/messages/MessagesSc.php | 993 +++--- languages/messages/MessagesScn.php | 2 +- languages/messages/MessagesSd.php | 2 +- languages/messages/MessagesSe.php | 1345 +++++++ languages/messages/MessagesSk.php | 555 +-- languages/messages/MessagesSl.php | 81 +- languages/messages/MessagesSn.php | 181 + languages/messages/MessagesSo.php | 585 ++++ languages/messages/MessagesSq.php | 2847 ++++++++------- languages/messages/MessagesSr.php | 2 +- languages/messages/MessagesSr_ec.php | 2848 +++++++-------- languages/messages/MessagesSr_el.php | 2854 +++++++-------- languages/messages/MessagesSr_jc.php | 2 +- languages/messages/MessagesSr_jl.php | 2 +- languages/messages/MessagesSu.php | 512 +-- languages/messages/MessagesSv.php | 709 ++-- languages/messages/MessagesTa.php | 1246 +++---- languages/messages/MessagesTe.php | 1568 +++++---- languages/messages/MessagesTg.php | 2 +- languages/messages/MessagesTh.php | 2279 +++++++++++- languages/messages/MessagesTi.php | 180 + languages/messages/MessagesTlh.php | 2 +- languages/messages/MessagesTn.php | 160 + languages/messages/MessagesTpi.php | 268 ++ languages/messages/MessagesTr.php | 2032 +++++------ languages/messages/MessagesTt.php | 384 +- languages/messages/MessagesTy.php | 2 +- languages/messages/MessagesTyv.php | 399 +-- languages/messages/MessagesUdm.php | 2 +- languages/messages/MessagesUg.php | 2 +- languages/messages/MessagesUk.php | 280 +- languages/messages/MessagesUr.php | 18 +- languages/messages/MessagesUz.php | 16 +- languages/messages/MessagesVec.php | 2287 ++++++------ languages/messages/MessagesVi.php | 2083 +++++------ languages/messages/MessagesVls.php | 2 +- languages/messages/MessagesVo.php | 104 + languages/messages/MessagesWa.php | 2573 +++++++------- languages/messages/MessagesWar.php | 136 + languages/messages/MessagesWo.php | 8 + languages/messages/MessagesXal.php | 2 +- languages/messages/MessagesYi.php | 1700 ++++++--- languages/messages/MessagesZa.php | 2 +- languages/messages/MessagesZea.php | 2 +- languages/messages/MessagesZh.php | 2 +- languages/messages/MessagesZh_classical.php | 845 +++-- languages/messages/MessagesZh_cn.php | 577 +-- languages/messages/MessagesZh_hk.php | 2 +- languages/messages/MessagesZh_sg.php | 2 +- languages/messages/MessagesZh_tw.php | 614 ++-- languages/messages/MessagesZh_yue.php | 3417 +++++++++--------- maintenance/FiveUpgrade.inc | 54 +- maintenance/README | 9 +- maintenance/addwiki.php | 2 +- maintenance/archives/patch-archive-page_id.sql | 6 + maintenance/archives/patch-archive-user-index.sql | 4 + maintenance/archives/patch-categorylinksindex.sql | 11 + maintenance/archives/patch-image-user-index.sql | 8 + maintenance/archives/patch-img_sha1.sql | 8 + maintenance/archives/patch-ipb_emailban.sql | 4 + maintenance/archives/patch-oi_metadata.sql | 17 + maintenance/archives/patch-oldimage-user-index.sql | 8 + maintenance/archives/patch-page_restrictions.sql | 2 - maintenance/archives/patch-rc_deleted.sql | 2 +- maintenance/archives/populateSha1.php | 43 + maintenance/archives/upgradeWatchlist.php | 2 +- maintenance/attachLatest.php | 2 +- maintenance/attribute.php | 2 +- maintenance/benchmarkPurge.php | 1 - maintenance/changePassword.php | 39 +- maintenance/checkUsernames.php | 2 +- maintenance/cleanupCaps.php | 2 +- maintenance/cleanupImages.php | 7 +- maintenance/cleanupSpam.php | 2 +- maintenance/cleanupTitles.php | 2 +- maintenance/cleanupWatchlist.php | 2 +- maintenance/clear_interwiki_cache.php | 2 +- maintenance/clear_stats.php | 2 +- maintenance/commandLine.inc | 16 +- maintenance/convertLinks.php | 2 +- maintenance/counter.php | 2 +- maintenance/createAndPromote.php | 27 +- maintenance/deleteArchivedFiles.inc | 56 + maintenance/deleteArchivedFiles.php | 30 + maintenance/deleteArchivedRevisions.inc | 35 + maintenance/deleteArchivedRevisions.php | 30 + maintenance/deleteBatch.php | 2 +- maintenance/deleteDefaultMessages.php | 6 +- maintenance/deleteImageMemcached.php | 2 +- maintenance/deleteOldRevisions.php | 1 - maintenance/deleteOrphanedRevisions.inc.php | 1 - maintenance/deleteOrphanedRevisions.php | 1 - maintenance/deleteRevision.php | 2 +- maintenance/dumpBackup.php | 5 +- maintenance/dumpHTML.inc | 4 + maintenance/dumpHTML.php | 62 +- maintenance/dumpInterwiki.php | 2 +- maintenance/dumpLinks.php | 2 +- maintenance/dumpSisterSites.php | 2 +- maintenance/dumpTextPass.php | 18 +- maintenance/dumpUploads.php | 1 - maintenance/edit.php | 2 +- maintenance/eval.php | 2 +- maintenance/findhooks.php | 2 +- maintenance/fixSlaveDesync.php | 2 +- maintenance/fixTimestamps.php | 2 +- maintenance/fixUserRegistration.php | 2 +- maintenance/fuzz-tester.php | 38 +- maintenance/generateSitemap.php | 6 +- maintenance/getLagTimes.php | 1 - maintenance/getSlaveServer.php | 2 +- maintenance/importDump.php | 3 +- maintenance/importImages.inc.php | 20 +- maintenance/importImages.php | 137 +- maintenance/importLogs.php | 2 +- maintenance/importTextFile.php | 1 - maintenance/importUseModWiki.php | 4 +- maintenance/initEditCount.php | 2 +- maintenance/initStats.php | 1 - maintenance/installExtension.php | 2 +- maintenance/interwiki.sql | 15 +- maintenance/language/alltrans.php | 2 +- maintenance/language/checkExtensions.php | 269 ++ maintenance/language/checkLanguage.inc | 99 +- maintenance/language/checkLanguage.php | 42 +- maintenance/language/date-formats.php | 2 +- maintenance/language/diffLanguage.php | 2 +- maintenance/language/digit2html.php | 24 + maintenance/language/dumpMessages.php | 2 +- maintenance/language/function-list.php | 2 +- maintenance/language/lang2po.php | 2 +- maintenance/language/langmemusage.php | 2 +- maintenance/language/messageTypes.inc | 70 +- maintenance/language/messages.inc | 250 +- maintenance/language/rebuildLanguage.php | 2 +- maintenance/language/splitLanguageFiles.php | 2 +- maintenance/language/transstat.php | 2 +- maintenance/language/validate.php | 2 +- maintenance/language/writeMessagesArray.inc | 25 +- maintenance/mcc.php | 3 +- maintenance/mctest.php | 4 +- maintenance/moveBatch.php | 2 +- maintenance/namespace2sql.php | 2 +- maintenance/namespaceDupes.php | 113 +- maintenance/nextJobDB.php | 22 +- maintenance/nukeNS.php | 1 - maintenance/nukePage.php | 1 - maintenance/ora/tables.sql | 16 +- maintenance/orphans.php | 1 - maintenance/ourusers.php | 2 +- maintenance/parserTests.inc | 68 +- maintenance/parserTests.php | 2 +- maintenance/parserTests.txt | 137 +- maintenance/parserTestsParserHook.php | 2 +- maintenance/parserTestsParserTime.php | 2 +- maintenance/parserTestsStaticParserHook.php | 2 +- maintenance/postgres/compare_schemas.pl | 14 +- maintenance/postgres/tables.sql | 26 +- maintenance/purgeList.php | 2 +- maintenance/purgeOldText.php | 1 - maintenance/reassignEdits.inc.php | 1 - maintenance/reassignEdits.php | 1 - maintenance/rebuildImages.php | 143 +- maintenance/rebuildInterwiki.php | 2 +- maintenance/rebuildall.php | 2 +- maintenance/rebuildrecentchanges.inc | 85 +- maintenance/rebuildrecentchanges.php | 3 +- maintenance/rebuildtextindex.php | 14 +- maintenance/refreshImageCount.php | 2 +- maintenance/refreshLinks.inc | 38 +- maintenance/refreshLinks.php | 4 +- maintenance/removeUnusedAccounts.php | 2 +- maintenance/renamewiki.php | 1 - maintenance/renderDump.php | 5 +- maintenance/runJobs.php | 21 +- maintenance/showJobs.php | 2 +- maintenance/showStats.php | 4 +- maintenance/sql.php | 4 +- maintenance/stats.php | 2 +- maintenance/storage/checkStorage.php | 5 +- maintenance/storage/compressOld.php | 2 +- maintenance/storage/dumpRev.php | 1 - maintenance/storage/moveToExternal.php | 2 +- maintenance/storage/resolveStubs.php | 3 +- maintenance/tables.sql | 50 +- maintenance/undelete.php | 33 + maintenance/update.php | 2 +- maintenance/updateArticleCount.inc.php | 2 +- maintenance/updateArticleCount.php | 1 - maintenance/updateRestrictions.php | 6 +- maintenance/updateSearchIndex.php | 2 +- maintenance/updateSpecialPages.php | 2 +- maintenance/updaters.inc | 209 +- maintenance/upgrade1_5.php | 2 +- maintenance/userOptions.inc | 27 +- maintenance/userOptions.php | 2 +- maintenance/waitForSlave.php | 2 +- maintenance/wikipedia-interwiki.sql | 2 + math/texutil.ml | 20 +- redirect.php | 3 +- redirect.php5 | 1 + serialized/serialize-localisation.php | 2 +- serialized/serialize.php | 2 +- skins/ArchLinux.deps.php | 3 +- skins/ArchLinux.php | 13 +- skins/Chick.deps.php | 4 +- skins/Chick.php | 2 +- skins/CologneBlue.php | 2 +- skins/MonoBook.deps.php | 4 +- skins/MonoBook.php | 11 +- skins/MySkin.deps.php | 4 +- skins/MySkin.php | 2 +- skins/Nostalgia.php | 2 +- skins/Simple.deps.php | 4 +- skins/Simple.php | 2 +- skins/Standard.php | 6 +- skins/archlinux/Opera95Fixes.css | 10 + skins/archlinux/main.css | 641 ++-- skins/archlinux/rtl.css | 12 +- skins/chick/main.css | 39 +- skins/common/ajax.js | 21 +- skins/common/ajaxwatch.js | 99 +- skins/common/block.js | 16 +- skins/common/cologneblue.css | 13 +- skins/common/commonPrint.css | 3 + skins/common/common_rtl.css | 9 +- skins/common/diff.css | 74 + skins/common/diff.js | 20 + skins/common/images/spinner.gif | Bin 0 -> 2285 bytes skins/common/oldshared.css | 465 +++ skins/common/protect.js | 172 +- skins/common/shared.css | 48 + skins/common/upload.js | 175 +- skins/common/wikibits.js | 292 +- skins/disabled/HTMLDump.php | 2 +- skins/disabled/MonoBook.tpl | 2 +- skins/disabled/MonoBookCBT.php | 2 +- skins/monobook/Opera95Fixes.css | 10 + skins/monobook/main.css | 637 ++-- skins/monobook/rtl.css | 8 + skins/monobook/user.gif | Bin 932 -> 923 bytes skins/simple/main.css | 28 +- t/00-test.t | 2 + t/inc/IP.t | 1 + t/inc/Language.t | 62 + t/inc/Licenses.t | 6 +- t/inc/Sanitizer.t | 3 + t/inc/Title.t | 2 +- t/inc/Xml.t | 3 +- t/maint/bom.t | 38 + t/maint/eol-style.t | 6 +- t/maint/php-lint.t | 4 +- t/maint/php-tag.t | 6 +- t/maint/unix-newlines.t | 23 +- tests/ArticleTest.php | 44 +- tests/DatabaseTest.php | 19 +- tests/GlobalTest.php | 53 +- tests/ImageFunctionsTest.php | 48 + tests/LocalFileTest.php | 90 + tests/Makefile | 6 +- tests/MediaWiki_TestCase.php | 51 + tests/README | 9 +- tests/SearchEngineTest.php | 37 +- tests/SearchMySQL4Test.php | 13 +- tests/run-test.php | 7 + thumb.php | 104 +- thumb.php5 | 1 + 739 files changed, 102791 insertions(+), 63477 deletions(-) create mode 100644 api.php5 create mode 100644 config/index.php5 create mode 100644 img_auth.php5 create mode 100644 includes/EmaillingJob.php create mode 100644 includes/EnotifNotifyJob.php create mode 100644 includes/FileDeleteForm.php create mode 100644 includes/FileRevertForm.php create mode 100644 includes/RefreshLinksJob.php create mode 100644 includes/SpecialMostlinkedtemplates.php create mode 100644 includes/SpecialUncategorizedtemplates.php create mode 100644 includes/WatchlistEditor.php create mode 100644 includes/api/ApiQueryAllLinks.php create mode 100644 includes/api/ApiQueryAllUsers.php create mode 100644 includes/api/ApiQueryCategories.php create mode 100644 includes/api/ApiQueryCategoryMembers.php create mode 100644 includes/api/ApiQueryExtLinksUsage.php create mode 100644 includes/api/ApiQueryExternalLinks.php create mode 100644 includes/api/ApiQueryImageInfo.php create mode 100644 includes/api/ApiQueryImages.php create mode 100644 includes/api/ApiQueryLangLinks.php create mode 100644 includes/api/ApiQueryLinks.php create mode 100644 includes/api/ApiQuerySearch.php create mode 100644 includes/api/ApiQueryUserInfo.php create mode 100644 includes/filerepo/ArchivedFile.php create mode 100644 includes/filerepo/FSRepo.php create mode 100644 includes/filerepo/File.php create mode 100644 includes/filerepo/FileRepo.php create mode 100644 includes/filerepo/FileRepoStatus.php create mode 100644 includes/filerepo/ForeignDBFile.php create mode 100644 includes/filerepo/ForeignDBRepo.php create mode 100644 includes/filerepo/ICRepo.php create mode 100644 includes/filerepo/LocalFile.php create mode 100644 includes/filerepo/LocalRepo.php create mode 100644 includes/filerepo/OldLocalFile.php create mode 100644 includes/filerepo/README create mode 100644 includes/filerepo/RepoGroup.php create mode 100644 includes/filerepo/UnregisteredLocalFile.php create mode 100644 index.php5 create mode 100644 languages/classes/LanguageBe_tarask.php create mode 100644 languages/classes/LanguageKu.deps.php create mode 100644 languages/classes/LanguageKu.php create mode 100644 languages/classes/LanguageKu_ku.php create mode 100644 languages/messages/MessagesBcl.php create mode 100644 languages/messages/MessagesBe_tarask.php create mode 100644 languages/messages/MessagesExt.php create mode 100644 languages/messages/MessagesFrc.php create mode 100644 languages/messages/MessagesFrp.php create mode 100644 languages/messages/MessagesGl.php create mode 100644 languages/messages/MessagesHak.php create mode 100644 languages/messages/MessagesHt.php create mode 100644 languages/messages/MessagesKrj.php create mode 100644 languages/messages/MessagesKu_arab.php create mode 100644 languages/messages/MessagesKu_latn.php create mode 100644 languages/messages/MessagesMn.php create mode 100644 languages/messages/MessagesMy.php create mode 100644 languages/messages/MessagesRoa_rup.php create mode 100644 languages/messages/MessagesSah.php create mode 100644 languages/messages/MessagesSe.php create mode 100644 languages/messages/MessagesSn.php create mode 100644 languages/messages/MessagesSo.php create mode 100644 languages/messages/MessagesTi.php create mode 100644 languages/messages/MessagesTn.php create mode 100644 languages/messages/MessagesTpi.php create mode 100644 languages/messages/MessagesVo.php create mode 100644 languages/messages/MessagesWar.php create mode 100644 languages/messages/MessagesWo.php create mode 100644 maintenance/archives/patch-archive-page_id.sql create mode 100644 maintenance/archives/patch-archive-user-index.sql create mode 100644 maintenance/archives/patch-categorylinksindex.sql create mode 100644 maintenance/archives/patch-image-user-index.sql create mode 100644 maintenance/archives/patch-img_sha1.sql create mode 100644 maintenance/archives/patch-ipb_emailban.sql create mode 100644 maintenance/archives/patch-oi_metadata.sql create mode 100644 maintenance/archives/patch-oldimage-user-index.sql create mode 100644 maintenance/archives/populateSha1.php create mode 100644 maintenance/deleteArchivedFiles.inc create mode 100644 maintenance/deleteArchivedFiles.php create mode 100644 maintenance/deleteArchivedRevisions.inc create mode 100644 maintenance/deleteArchivedRevisions.php create mode 100644 maintenance/language/checkExtensions.php create mode 100644 maintenance/language/digit2html.php create mode 100644 maintenance/undelete.php create mode 100644 redirect.php5 create mode 100644 skins/archlinux/Opera95Fixes.css create mode 100644 skins/common/diff.css create mode 100644 skins/common/diff.js create mode 100644 skins/common/images/spinner.gif create mode 100644 skins/common/oldshared.css create mode 100644 skins/common/shared.css create mode 100644 skins/monobook/Opera95Fixes.css create mode 100644 t/inc/Language.t create mode 100644 t/maint/bom.t create mode 100644 tests/ImageFunctionsTest.php create mode 100644 tests/LocalFileTest.php create mode 100644 tests/MediaWiki_TestCase.php create mode 100644 tests/run-test.php create mode 100644 thumb.php5 diff --git a/HISTORY b/HISTORY index 6e710a09..f98333a7 100644 --- a/HISTORY +++ b/HISTORY @@ -1,4 +1,494 @@ -Change notes from older releases. For current info see RELEASE-NOTES. +Change notes from older releases. For current info see RELEASE-NOTES. + +== MediaWiki 1.10 == + +This is the Spring 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 == + +* A new switch $wgCommandLineDarkBg used by maintenance scripts (parserTests.php). + It lets you specify if your terminal use a dark background, the colorized + output will be made lighter making things easier to read. +* The minimum permissions needed to edit a page in each namespace can now be + customized via the $wgNamespaceProtection array. By default, editing pages in + the MediaWiki namespace requires "editinterface" permission, as before. +* Allow restriction of autoconfirmed permission by edit count. New global setting + $wgAutoConfirmCount (defaulting to zero, naturally). +* Added rate limiter for Special:Emailuser +* Private logs can now be created using $wgLogRestrictions +* (Bug 8590) limited HTML is now always enabled ($wgUserHtml = true). +* Deprecated $wgUseImageResize, thumbnailing will be enabled unconditionally. + +== New features since 1.9 == + +* (bug 6937) Introduce "statistics-footer" message, appended to + Special:Statistics +* (bug 6638) List block flags in block log entries +* (bugs 5051, 5376) Tooltips and accesskeys no longer require JavaScript +* Added SkinTemplateOutputPageBeforeExec hook before SkinTemplate::outputPage() + starts page output + (http://lists.wikimedia.org/pipermail/wikitech-l/2007-January/028554.html) +* Introduce "cascading protection" -- implicit protection on pages transcluded + into a page protected with this option enabled +* (bug 8567) Added hook RawPageViewBeforeOutput just before the text is blown + out in action=raw, so extensions might influence the output. +* (bug 3446) Add user preference to hide page content below diffs, can be + overridden by adding diffonly=1 or diffonly=0 to the URL of the diff page +* Add 'purge' privilege to replace the hardcoded check for login state in + determining whether action=purge can be done via GET. Switching the + permission on for anons can be helpful for benchmarking. +* (bug 7842) Link back to deleted revision list from deleted revision preview +* (bug 8619) Add user-aware "unblock" link to Special:Blockip +* (bug 8522) Provide a "delete" link on Special:Brokenredirects for users with + the appropriate permission +* (bug 8628) Add user-aware block list link to Special:Blockip +* (bug 8621) Log revisions marked as patrolled +* Introduce "BookInformation" hook; see docs/hooks.txt for more details +* Add title prefix search for Special:Undelete +* Remove full-archive list from Special:Undelete +* (bug 8136) Introduce 'ArticleUndelete' hook; see docs/hooks.txt for more info +* (bug 8712) Expose user groups as a JavaScript global +* Introduce 'CustomEditor' hook; see docs/hooks.txt for more information +* New special page, Special:Protectedpages, which shows all protected pages + and their protection status (full protection status is not pulled out due + to performance considerations, so it just shows "full protected" or + "semi protected". +* (bug 4133) Allow page protections to be made with an expiry date, in the same + format as block expiry dates. Existing protections are assumed to be infinite, + as are protections made with the new field left blank. +* (bug 8535) Allow certain vertical alignment attributes to be used as image + keywords +* (bug 6987) Allow perrow, widths, and heights attributes for +* (bug 3678) Allow disabling MediaWiki:Aboutsite in the same way as + MediaWiki:Disclaimers; Also means that if any of the footer links are + disabled in the wiki's default language (by setting to "-"), they'll also + be disabled in other languages too (e.g. if the user specifies uselang=fr). +* Sort log types in Special:Log +* Added a classname ("mw-toolbar-editbutton") and unique IDs to the edit + toolbar buttons +* Hide irrelevant block options in Special:Blockip based on whether an + IP address/range or username is listed. (Dynamic using JS.) +* (bug 9032) Make quickbarSettings localizable through Special:Allmessages +* (bug 7782) Standardisation of file info at image description pages. +* (bug 1035) View contributions / recentchanges for an IP range. +* (bug 8747) When unwatching pages from Special:Watchlist/edit, put the + confirmation messages in a proper list with a CSS class and id. +* (bug 9118) Show relevant log fragments on deletion confirmatio page +* (bug 9009) Add username entry field to Special:Contributions +* (bug 1723) Article size in history +* (bug 9223) Disallow magic tilde sequences in page titles and usernames +* (bug 6997) Link from Special:log/block to unblock form +* (bug 9117) Link from Special:log/delete to undelete form +* Link from Special:log/protect to change protection form +* (bug 1196) Add IPv6 support added to blocks, more consistancy for IPv6 contribs +* (bug 3984) Searching in logs by title% +* Show thumbnail of existing image if image exists already under this filename +* (bug 5546) Watchlist reflects logged actions like move, protection, undelete +* Support protocols other than HTTP in LinkFilter, use $wgUrlProtocols +* (bug 3069) Warning on upload of scaled down images +* Warning on upload of images with uppercase extension if image with lowercase + extension exists +* (bug 4624) Namespace selection for Special:Whatlinkshere +* Introduce PageHistoryBeforeList and PageHistoryLineEnding hooks; see docs/hooks.txt + for more information +* (bug 9397) Introduce "sp-contributions-footer" and "sp-contributions-footer-anon" + messages, shown at the end of Special:Contributions as appropriate for the target +* (bug 8421) Expose current action in JavaScript globals (as 'wgAction') +* (bug 9069) Use galleries in query pages dedicated to images +* (bug 9177) Installer now warns of various conditions affecting session.save_path + which can lead to broken session storage +* (bug 9046) Special page to list pages without language links +* (bug 9508) Special page to list articles with the fewest revisions +* Introduce 'FileUpload' hook; see docs/hooks.txt for more information +* Introduce 'SearchUpdate' hook; see docs/hooks.txt for more information +* Introduce 'mywatchlist' message; used on personal menu to link to watchlist page +* Introduce magic word {{NUMBEROFEDITS}} +* Introduced media handlers for file-type specific operations. +* Improved error reporting for image thumbnailing +* Added sharpening option for ImageMagick thumbnailing +* (bug 9656) Autosummaries will be generated for deletion of pages longer than + 500 characters +* Predefined block reasons added to Special:Blockip +* (bug 9196) Installer now check that zend.ze1_compatibility_mode is off +* (bug 9697) Introduce 'InternalParseBeforeLinks' hook; see docs/hooks.txt for more information +* 'contribsub' message changed to 'contribsub2' with two parameters to permit + better localization. Change is reverse-compatible and can be ignored for + most wikis. +* Adding a 'reason' field to Special:Userrights + +== Bugfixes since 1.9 == + +* (bug 7292) Fix site statistics when moving pages in/out of content namespaces +* (bug 8531) Correct local name of Lingála +* Made the PLURAL: parser function return singular on -1 per default +* Fixed up the AjaxSearch +* Fix SpecialVersion->formatCredits input. Version and Url parameters should be + null to be treated properly with isset. +* Page restrictions moved into a new, dedicated table +* Correct tooltip accesskey hint for Opera on the Macintosh + (uses Shift-Esc-, not Ctrl-). +* (bug 8002) Math should render left-to-right even in right-to-left wikis +* Pass e-mail and real name fields to AuthPlugin::addUser, as additional + optional fields, which may be considered useful at registration time. +* PostgreSQL upgrade scripts fixed and updated +* (bug 8613) Fix error when viewing "Recent Changes" and using Postgres. +* Initialise site_stats table at upgrade time if data was missing +* (bug 7250) Updated Unicode normalization tables to Unicode 5.0 +* Unmaintained Oracle support files have been removed. +* Use browser default for printing size, don't force to 11pt +* (bug 8632) Fix regression in page protection null edit update +* (bug 8407) Disallow indexing of "printable" versions +* (bug 8643) Correctly escape the page-specific CSS class for non-Monobook skins +* (bug 8629) Document $wgFilterCallback +* (bug 1000) Clarify warning about memory_limit in installer +* Suppress PHP warning about set_time_limit in installer when safe mode is on +* (bug 3000) Fall back to SCRIPT_NAME plus QUERY_STRING when REQUEST_URI is + not available, as on IIS with PHP-CGI +* Missing interwiki row for English Wikipedia restored (as "wikipedia:") +* use configured cache servers for mctest.php +* bucket details in mcc.php +* fix input validation and remove debugging code in compressOld +* full ID range for moveToExternal +* fix resolveStubs.php for compatibility with older serialized data +* maximum line length for bar graphs in getLagTimes.php +* recognize specieswiki in rebuildInterwiki.inc +* profile unicode cleanup in Xml +* log slow parses in Article.php +* profile wfMsgReal +* log mkdir failures +* profile AutoLoader +* rebuild empty DjVu metadata containing '' +* security fix for DjVu metadata retrieval +* Undelete page list can use plural marker +* (bug 8638) Fix update from 1.4 and earlier +* (bug 8641) Fix order of updates to ipblocks table +* (bug 8678) Fix detection of self-links for numeric titles in Parser +* (bug 6171) Magically close tags in tables when not using Tidy. +* Sanitizer now correctly escapes lonely '>' occurring before the first wikitag. +* Ignore self closing on closing tags ( '' now gives '') +* (bug 8673) Minor fix for web service API content-type header +* Fix API revision list on PHP 5.2.1; bad reference assignment +* (bug 8688) Handle underscores/spaces in Special:Blockip and Special:Ipblocklist + in a consistent manner +* (bug 8701) Check database lock status when blocking/unblocking users +* ParserOptions and ParserOutput classes are now in their own files +* (bug 8708) Namespace translations for Zealandic language +* Renamed constructor methods to PHP 5 __construct reserved name +* (bug 8715) Warn users when editing an interface message whether or not the + message page exists +* ar: fix the 'create a new page' on search page when no exact match found +* (bug 8703) Corrected talk and image namespace name for Limburgish (li) +* (bug 8671) Expose "wpDestFile" as a parameter to "uploadtext" +* (bug 8403) Respect bad image list exceptions in galleries on wiki pages +* Allow sending per-user contribution requests to "contributions" query group +* (bug 3717) Update user count for AuthPlugin account autocreation +* (bug 8719) Firefox release notes lie! Fix tooltips for Firefox 2 on x11; + accesskeys default settings appear to be same as Windows. +* Added an option to make Linker::userToolLinks() show the contribs link + red when the user has no edits. Linker::userToolLinksRedContribs() is an + alias to that which should be used to make it more self documentating. +* (bug 8749) Bring MySQL 5 table defs back into sync +* (bug 8751) Set session cookies to HTTPS-only to match other cookies +* (bug 8652) Catch exceptions generated by malformed XML in multipage media +* (bug 8782) Help text in Makefile +* (bug 8777) Suppress 'previous' link on Special:Allpages when at first page +* (bug 8774) Fix path for GNU FDL rights icon on new installs +* Fix multipage selector drop-down for DjVu images to work when title + is passed as a query string parameter; we have to pass the title as + a form parameter or it gets dropped from the form submission URL +* (bug 8819) Fix full path disclosure in with skins dependencies +* Fixed bug affecting HTML formatting in sortable table column titles +* Merged table sorting code into wikibits.js +* (bug 8711) Stop floats in previews from spilling into edit area +* (bug 8858) Safer handling when $wgImageLimits is changed. Added a note + in DefaultSettings to make it clear. +* (bug 4268) Fixed data-loss bug in compressOld batch text compression + affecting pages which had null edits (move, protect, etc) as second + edit in a batch group. Isolated and patched by Travis Derouin. +* Fix for paths in 1.4->1.5 special-case updater script +* (bug 8789) AJAX search: IE users can now use the return key +* (bug 6844) Use and tags to emphase the differences +* (bug 6684) Fix improper javascript array iteration +* (bug 4347) use MailAddress object for reply-to +* Add AlphabeticPager abstract class +* Use faster AlphabeticPager for Special:Categories +* (bug 8875) Show printable link in MonoBook sidebar for locally nonexistent + pages; perhaps useful for categories and shared images +* Clean up session checks to better handle the case where the session was + opened during the current request. May help with some caching corner + cases. +* (bug 8897) Fix whitespace removal for interlanguage links with link prefix +* Add 'ParserTestTables' hook to expand the list of temporary tables copied + by the parser test harness; use for extensions which require the presence + of other tables while they work. +* Message names changed for AlphabeticPager introduced with r19758 + for better localisations. +* (bug 8944) The deprecated is_a() function is used in StubObjects.php +* (bug 8992) Fix a remaining raw use of REQUEST_URI in history +* (bug 8999) User.php gives "undefined user editcount" PHP notice. +* (bug 8984) Fix a database error in Special:Recentchangeslinked + when using the Postgres database. +* Moved the main ob_start() from the default LocalSettings.php to WebStart.php. + The ob_start() section should preferably be removed from older + LocalSettings.php files. +* Give Content-Length header for HTTP/1.0 clients. +* Partial support for Flash cross-domain-policy filtering. +* Lazy-initialize site_stats row on load when empty. Somewhat kinder to + dump-based installations, avoiding PHP warnings when NUMBEROFARTICLES + and such are used. +* Add 'charset' to Content-Type headers on various HTTP error responses + to forestall additional UTF-7-autodetect XSS issues. PHP sends only + 'text/html' by default when the script didn't specify more details, + which some inconsiderate browsers consider a license to autodetect + the deadly, hard-to-escape UTF-7. + This fixes an issue with the Ajax interface error message on MSIE when + $wgUseAjax is enabled (not default configuration); this UTF-7 variant + on a previously fixed attack vector was discovered by Moshe BA from BugSec: + http://www.bugsec.com/articles.php?Security=24 +* Trackback responses now specify XML content type +* (bug 9044) Send a comment with action=raw pages in CSS/JS output mode + to work around IE/Mac bug where empty pages time out verrrrryyyyy slowly, + particularly with new keepalive-friendly HTTP on Wikipedia +* (bug 8919) Suppress paging links and related messages where there are no + rows to list for query pages +* (bug 9057) Standardize MediaWiki: namespace for oc +* (bug 8132) Suppress "Pages in this category" heading in categories when + there are none +* (bug 8958) Handle search operators better when using tsearch2 (Postgres) +* (bug 8799) Use redirect table for Special:BrokenRedirects and + Special:DoubleRedirects +* (bug 8918) Enable PLURAL option for MediaWiki:showingresults and + MediaWiki:showingresultsnum +* (bug 9122) Fix minor display issue in RTL with section edit link margin +* (bug 5805) Enable PLURAL option for some messages of watchlist and statistic +* (bug 3953) Work around poor display of parenthesis in the in other + languages section of MonoBook skin +* (bug 8539) Enable PLURAL option for another message of recentchanges. +* (bug 8728) MediaWiki:Badfiletype splitted into 3 messages +* (bug 9131) Allow SpecialContributions to work with Postgres +* (bug 9155) Allow footer info to wrap in Monobook +* (bug 8847) Strip spurious #fragments from request URI to fix redirect + loops on some server configurations +* (bug 9097) column "pr_pagetype" does not exist +* (bug 9217) Balance wfProfile calls in Skin::outputPage +* (bug 9222) PostgreSQL updater should not be version-specific +* Fix fallback implementation of mb_strlen so it works and isn't insanely + slow for large strings, since it's used for page edit lengths +* (bug 8815) Setting password in initUser() breaks LdapAuthentication plugin +* (bug 9256) Add a quick note to index.php header comments +* Make Special:Listusers caseinsensitive for first letter +* Default tidy.conf has been moved from extensions module into includes. +* Ignore lonely ''''' +* (bug 9244) When calling edit page for nonexistent section, generate error + inside of just discarding edits, since edit links sometimes go to the wrong + place. +* (bug 9019) No warning during upload if image description page exists, but no + image +* (bug 8582) Allow thumbnailing when imagesize has a space. +* (bug 8716) Change math_inputhash and math_outputhash to bytea for Postgres +* (bug 9343) Correct internal name for Wolof language +* (bug 9363) Fix Postgres error on Recentchangeslinked +* (bug 5142) Fixed call of hook ArticleViewHeader +* (bug 4777) Separate prev/next messages for Special:Whatlinkshere +* Merge approx 15 missing Wikipedia language codes into wikipedia-interwiki.sql + based on Jeff Merkey's mediawiki-1.9.3.WG-20070316.tar.gz.bz2 archive. +* (bug 9411) Fix for shared image descriptions using query-string titles +* (bug 4756) Add user tool links for self created accounts at special:log + instead of sometimes broken block links from newuserlog extension +* (bug 5817) Special:Recentchangeslinked now shows red link for nonexistent + target page instead of silently redirecting +* (bug 8914) Don't transform colons in {{anchorencode:}} +* (bug 9241) Handle edit section links and include size links for cached + templates the same as the first transclusion. +* (bug 9466) "Rollback failed" page doesn't format edit comment +* (bug 9472) Invalid XHTML on cached special pages +* (bug 9472) Invalid XHTML on Special:Newpages +* (bug 4764) "My contributions" not bold when viewing own contributions +* (bug 9194) Add {{PLURAL:...}} to navigation bar of Special:Whatlinkshere +* (bug 9033) Use a more specific error message when users are not able/allowed + to edit page protection levels due to a block, database lock or permissions +* Fixed $wgFeedLimit +* (bug 9270) Corrected help namespace name for Dutch Lower Saxon (nds-nl) +* (bug 929, 4215) Expose "rcdays" user preference in Special:Preferences +* (bug 9554) Extension-provided group name messages not used +* (bug 9565) Translate template namespace name for Hindi (hi) +* (bug 8599) Correct localized names of zh-variants +* (bug 3366) Require skins based on SkinTemplate to override the skinname + property. +* (bug 9220) Removed obsoletes functions in install-utils.inc. +* Removed obsoletes Title::getRelatedCache and Title:touchArray +* (bug 7285) Check MySQL username length during install +* (bug 6910) Correct date/time formats in Vietnamese (vi) +* (bug 9608) Correctly use ORDER BY in dumpLinks.php +* (bug 9609) Correctly use ORDER BY in SpecialWhatlinkshere.php +* Special:Random and Special:Randomredirect now try harder to send the user to + a random page, and will give an error message if none really can be found + instead of sending the user to the main page like they used to +* Fix object variable used for displaying "not-patrolled" CSS class on list +* Fixed interaction of page parameter to ImagePage with the HTML file cache +* Fixed MIME type for SVG files, will be silently changed from image/svg + to image/svg+xml after loading from the database. +* Workaround for djvutoxml bug #1704049 (poor performance). Use djvudump + instead. +* Fixed odd behaviour in ImagePage on DjVu thumbnailing errors +* (bug 5439) "Go" title search will now jump to shared/foreign Image: and + MediaWiki: pages that have not been locally edited. +* (bug 9630) Limits links in Whatlinkshere forgot about namespace filter +* Fixed upgrade for the non-standard MySQL schemas +* Disable MySQL's strict mode at session start for MySQL 4.1+, to avoid the + various problems that occur when it is on. +* (bug 9585) Fix regression in tidy usage in Special:Undelete previews +* (bug 3826) Normalize some invalid cookie name characters when setting + up $wgCookiePrefix. Completes application of patch by Anders Kaseorg. +* (bug 9649) Fix RTL form alignment for Special:Movepage +* (bug 9582) Members of bot group now mark edits patrolled by default +* (bug 9669) Fix limit ordering for rebuildrecentchanges; broken since + converted from 1.4 to 1.5 schema +* (bug 9682) Revert PHP 5.1 dependency on warning suppression for SVN info +* (bug 5959) Anchors dropped from stub links +* (bug 3348) Some additional weak password checks: password which is same + as username will now be rejected. +* (bug 8602) Converted Special:Contributions to use an IndexPager. The + interpretation of the offset parameter has changed, and the go parameter + has been removed. +* (bug 6204) Fixes for indentation with $wgMaxTocLevel: + - don't emit too many list close tags after an invisible header + - don't emit too many final list close tags if last header is invisible + - don't emit TOC when there are no visible headers +* (bug 7629) Fix $wgBrowserBlackList to avoid false positive on MSIE + when certain plugins are present which alter the user agent + + +== Maintenance == + +* New script maintenance/language/checkExtensioni18n.php used to check i18n + progress in the extension repository. +* Running maintenance/parserTests.php with '--record' option, will now + automatically attempt to create the required tables +* --purge option to do additional parser-cache purging for purgeList.php +* Fix hardcoded background color in parserTests.php +* parserTests.php : removed the 'light' option for --color argument, replacing + it with a new global switch : $wgCommandLineDarkBg +* (bug 8780) Clarify message for command-line scripts if LocalSettings.php + exists but is not readable +* dumpBackup / importDump now work with PostgreSQL +* (bug 8975) Use "Maintenance script" as the default username for importImages.php + and importTextFile.php scripts +* (bug 8933) Fix maintenance/reassignEdits.php script +* (bug 9440) Added "mediawikiwiki" interwiki prefix to MediaWiki.org +* (bug 2979) Import now gracefully skips invalid titles with a warning +* Restore '--norc' option for maintenance/importTextFile.php +* Help information for maintenance/importTextFile.php now easier to read on + consoles +* Doxygen documentation now show the revision number of each file, generate + graphs using dot and include a search engine. + + +== Languages updated == + +* Arabic (ar) +* Aramaic (arc) +* Aymara (ay) +* Belarusian normative (be) +* Belarusian alternative (be-x-old) +* Bulgarian (bg) +* Bihara (bh) +* Breton (br) +* Catalan (ca) +* Czech (cs) +* Danish (da) +* German (de) +* Greek (el) +* Esperanto (eo) +* Spanish (es) +* Estonian (et) +* Basque (eu) +* Finnish (fi) +* Võro (fiu-vro) +* French (fr) +* Hebrew (he) +* Hindi (hi) +* Upper Sorbian (hsb) +* Hungarian (hu) +* Armenian (hy) +* Indonesian (id) +* Italian (it) +* Japanese (ja) +* Javanese (jv) +* Georgian (ka) +* Kabyle (kab) +* Kazakh (kk) +* Korean (ko) +* Kashmiri (ks) +* Ripuarian (ksh) +* Latin (la) +* Luganda (lg) +* Limburgish (li) +* Lithuanian (lt) +* Latvian (lv) +* Marathi (mr) +* Low Saxon (nds) +* Dutch Lower Saxon (nds-nl) +* Nepali (ne) +* Nepal Bhasa (new) +* Dutch (nl) +* Occitan (oc) +* Pali (pi) +* Polish (pl) +* Romanian (ro) +* Russian (ru) +* Sanskrit (sa) +* Sicilian (scn) +* Slovak (sk) +* Sundanese (su) +* Swedish (sv) +* Tahitian (ty) +* Ukrainian (uk) +* Urdu (ur) +* Uzbek (uz) +* Vietnamese (vi) +* Zealandic (zea) +* Old Chinese / Late Middle Chinese (zh-classical) +* Chinese (PRC) (zh-cn) +* Chinese (Taiwan) (zh-tw) +* Cantonese (zh-yue) + +== Compatibility == + +MediaWiki 1.10 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 +Upgrade affected systems to PHP 5.1 or higher. + +MySQL 3.23.x is no longer supported; some older hosts may need to upgrade. +At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases. + + +== Upgrading == + +1.10 has several database changes since 1.9, 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 you are upgrading from MediaWiki 1.4.x or earlier, some major database +changes are made, and there is a slightly higher chance that things could +break. Don't forget to always back up your database before upgrading! + +See the file UPGRADE for more detailed upgrade instructions. = MediaWiki release notes = diff --git a/Makefile b/Makefile index b659cad2..b9d5b1cf 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,24 @@ # This Makefile is used to test some MediaWiki functions. If you # want to install MediaWiki, point your browser to ./config/ # + +# Configuration: +PROVE_BIN="prove" + +# Describe our tests: +BASE_TEST=$(wildcard t/*.t) +INCLUDES_TESTS=$(wildcard t/inc/*t) +MAINTENANCE_TESTS=$(wildcard t/maint/*t) + +# Build groups: +FAST_TESTS=$(BASE_TEST) $(INCLUDES_TESTS) +ALL_TESTS=$(BASE_TEST) $(INCLUDES_TESTS) $(MAINTENANCE_TESTS) + test: Test.php - prove -r t + $(PROVE_BIN) $(ALL_TESTS) + +fast: Test.php + $(PROVE_BIN) $(FAST_TESTS) -verbose: - prove -v -r t | egrep -v '^ok' +verbose: Test.php + $(PROVE_BIN) -v $(ALL_TESTS) | egrep -v '^ok' diff --git a/RELEASE-NOTES b/RELEASE-NOTES index ac1b7a47..94fec251 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -3,11 +3,25 @@ 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.10.2 == +== MediaWiki 1.11.0 == September 10, 2007 -This is a security fix update to the Spring 2007 quarterly release snapshot. +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 +"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 + +== Changes since 1.11.0rc1 == + A possible HTML/XSS injection vector in the API pretty-printing mode has been found and fixed. @@ -17,6 +31,8 @@ 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 @@ -34,515 +50,574 @@ the faulty function, however the BotQuery extension is similarly vulnerable unless updated to the latest SVN version. -== MediaWiki 1.10.1 == - -July 13, 2007 - -This is a bugfix update to the Spring 2007 quarterly release snapshot. -A number of fixes to improve compatibility with PostgreSQL, some versions -of MySQL, and some PHP configurations are included. - -Changes since 1.10.0: - +== 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.
-* Fixed installation on MyISAM or old InnoDB with charset=utf8, was giving 
-  overlong key errors.
-* Fixed zero-padding issues with MySQL 5 binary schema
+* 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"
-
-
-== MediaWiki 1.10.0 ==
-
-May 9, 2007
-
-This is the quarterly release snapshot for Spring 2007. See below for a full
-list of changes since the 1.9.x series.
-
-Changes since 1.10.0rc2:
-
-* (bug 9808) Fix regression that ignored user 'rclimit' option for Special:Contributions
-
-
-== MediaWiki 1.10.0rc2 ==
-
-May 4, 2007
-
-THIS IS A RELEASE CANDIDATE MADE AVAILABLE FOR TESTING!
-A FINAL 1.10.0 RELEASE WILL APPEAR WITHIN A FEW DAYS.
-
-Changes since 1.10.0rc1:
-* Various l10n fixes and updates
-* Fix for upgrade of page_restrictions table
-* (bug 9780) Fix normalization of titles with initial colon followed by whitespace
-* Fix for regression in upload: wrong size info saved into image table
-* Avoid cyclic stub problems when authorization hooks do funny things with
-  the user and the database at load time
-
-== MediaWiki 1.10.0rc1 ==
-
-April 30, 2007
-
-THIS IS A RELEASE CANDIDATE MADE AVAILABLE FOR TESTING!
-A FINAL 1.10.0 RELEASE WILL APPEAR WITHIN A FEW DAYS.
-
-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 ==
-
-* A new switch $wgCommandLineDarkBg used by maintenance scripts (parserTests.php).
-  It lets you specify if your terminal use a dark background, the colorized
-  output will be made lighter making things easier to read.
-* The minimum permissions needed to edit a page in each namespace can now be 
-  customized via the $wgNamespaceProtection array. By default, editing pages in
-  the MediaWiki namespace requires "editinterface" permission, as before.
-* Allow restriction of autoconfirmed permission by edit count. New global setting
-  $wgAutoConfirmCount (defaulting to zero, naturally).
-* Added rate limiter for Special:Emailuser
-* Private logs can now be created using $wgLogRestrictions
-* (Bug 8590) limited HTML is now always enabled ($wgUserHtml = true).
-* Deprecated $wgUseImageResize, thumbnailing will be enabled unconditionally.
-
-== New features since 1.9 ==
-
-* (bug 6937) Introduce "statistics-footer" message, appended to 
-  Special:Statistics
-* (bug 6638) List block flags in block log entries
-* (bugs 5051, 5376) Tooltips and accesskeys no longer require JavaScript
-* Added SkinTemplateOutputPageBeforeExec hook before SkinTemplate::outputPage()
-  starts page output
-  (http://lists.wikimedia.org/pipermail/wikitech-l/2007-January/028554.html)
-* Introduce "cascading protection" -- implicit protection on pages transcluded
-  into a page protected with this option enabled
-* (bug 8567) Added hook RawPageViewBeforeOutput just before the text is blown
-  out in action=raw, so extensions might influence the output.
-* (bug 3446) Add user preference to hide page content below diffs, can be
-  overridden by adding diffonly=1 or diffonly=0 to the URL of the diff page
-* Add 'purge' privilege to replace the hardcoded check for login state in
-  determining whether action=purge can be done via GET. Switching the
-  permission on for anons can be helpful for benchmarking.
-* (bug 7842) Link back to deleted revision list from deleted revision preview
-* (bug 8619) Add user-aware "unblock" link to Special:Blockip
-* (bug 8522) Provide a "delete" link on Special:Brokenredirects for users with
-  the appropriate permission
-* (bug 8628) Add user-aware block list link to Special:Blockip
-* (bug 8621) Log revisions marked as patrolled
-* Introduce "BookInformation" hook; see docs/hooks.txt for more details
-* Add title prefix search for Special:Undelete
-* Remove full-archive list from Special:Undelete
-* (bug 8136) Introduce 'ArticleUndelete' hook; see docs/hooks.txt for more info
-* (bug 8712) Expose user groups as a JavaScript global
-* Introduce 'CustomEditor' hook; see docs/hooks.txt for more information
-* New special page, Special:Protectedpages, which shows all protected pages
-  and their protection status (full protection status is not pulled out due
-  to performance considerations, so it just shows "full protected" or
-  "semi protected".
-* (bug 4133) Allow page protections to be made with an expiry date, in the same
-  format as block expiry dates. Existing protections are assumed to be infinite,
-  as are protections made with the new field left blank. 
-* (bug 8535) Allow certain vertical alignment attributes to be used as image
-  keywords
-* (bug 6987) Allow perrow, widths, and heights attributes for 
-* (bug 3678) Allow disabling MediaWiki:Aboutsite in the same way as
-  MediaWiki:Disclaimers; Also means that if any of the footer links are
-  disabled in the wiki's default language (by setting to "-"), they'll also
-  be disabled in other languages too (e.g. if the user specifies uselang=fr).
-* Sort log types in Special:Log
-* Added a classname ("mw-toolbar-editbutton") and unique IDs to the edit
-  toolbar buttons
-* Hide irrelevant block options in Special:Blockip based on whether an
-  IP address/range or username is listed. (Dynamic using JS.)
-* (bug 9032) Make quickbarSettings localizable through Special:Allmessages
-* (bug 7782) Standardisation of file info at image description pages.
-* (bug 1035) View contributions / recentchanges for an IP range.
-* (bug 8747) When unwatching pages from Special:Watchlist/edit, put the
-  confirmation messages in a proper list with a CSS class and id.
-* (bug 9118) Show relevant log fragments on deletion confirmatio page
-* (bug 9009) Add username entry field to Special:Contributions
-* (bug 1723) Article size in history
-* (bug 9223) Disallow magic tilde sequences in page titles and usernames
-* (bug 6997) Link from Special:log/block to unblock form
-* (bug 9117) Link from Special:log/delete to undelete form
-* Link from Special:log/protect to change protection form
-* (bug 1196) Add IPv6 support added to blocks, more consistancy for IPv6 contribs
-* (bug 3984) Searching in logs by title%
-* Show thumbnail of existing image if image exists already under this filename
-* (bug 5546) Watchlist reflects logged actions like move, protection, undelete
-* Support protocols other than HTTP in LinkFilter, use $wgUrlProtocols
-* (bug 3069) Warning on upload of scaled down images
-* Warning on upload of images with uppercase extension if image with lowercase
-  extension exists
-* (bug 4624) Namespace selection for Special:Whatlinkshere
-* Introduce PageHistoryBeforeList and PageHistoryLineEnding hooks; see docs/hooks.txt
-  for more information
-* (bug 9397) Introduce "sp-contributions-footer" and "sp-contributions-footer-anon"
-  messages, shown at the end of Special:Contributions as appropriate for the target
-* (bug 8421) Expose current action in JavaScript globals (as 'wgAction')
-* (bug 9069) Use galleries in query pages dedicated to images
-* (bug 9177) Installer now warns of various conditions affecting session.save_path
-  which can lead to broken session storage
-* (bug 9046) Special page to list pages without language links
-* (bug 9508) Special page to list articles with the fewest revisions
-* Introduce 'FileUpload' hook; see docs/hooks.txt for more information
-* Introduce 'SearchUpdate' hook; see docs/hooks.txt for more information
-* Introduce 'mywatchlist' message; used on personal menu to link to watchlist page
-* Introduce magic word {{NUMBEROFEDITS}}
-* Introduced media handlers for file-type specific operations.
-* Improved error reporting for image thumbnailing
-* Added sharpening option for ImageMagick thumbnailing
-* (bug 9656) Autosummaries will be generated for deletion of pages longer than
-  500 characters
-* Predefined block reasons added to Special:Blockip
-* (bug 9196) Installer now check that zend.ze1_compatibility_mode is off
-* (bug 9697) Introduce 'InternalParseBeforeLinks' hook; see docs/hooks.txt for more information
-* 'contribsub' message changed to 'contribsub2' with two parameters to permit
-  better localization.  Change is reverse-compatible and can be ignored for
-  most wikis.
-* Adding a 'reason' field to Special:Userrights
-
-== Bugfixes since 1.9 ==
-
-* (bug 7292) Fix site statistics when moving pages in/out of content namespaces
-* (bug 8531) Correct local name of Lingála
-* Made the PLURAL: parser function return singular on -1 per default
-* Fixed up the AjaxSearch
-* Fix SpecialVersion->formatCredits input. Version and Url parameters should be
-  null to be treated properly with isset.
-* Page restrictions moved into a new, dedicated table
-* Correct tooltip accesskey hint for Opera on the Macintosh
-  (uses Shift-Esc-, not Ctrl-).
-* (bug 8002) Math should render left-to-right even in right-to-left wikis
-* Pass e-mail and real name fields to AuthPlugin::addUser, as additional
-  optional fields, which may be considered useful at registration time.
-* PostgreSQL upgrade scripts fixed and updated
-* (bug 8613) Fix error when viewing "Recent Changes" and using Postgres.
-* Initialise site_stats table at upgrade time if data was missing
-* (bug 7250) Updated Unicode normalization tables to Unicode 5.0
-* Unmaintained Oracle support files have been removed.
-* Use browser default for printing size, don't force to 11pt
-* (bug 8632) Fix regression in page protection null edit update
-* (bug 8407) Disallow indexing of "printable" versions
-* (bug 8643) Correctly escape the page-specific CSS class for non-Monobook skins
-* (bug 8629) Document $wgFilterCallback
-* (bug 1000) Clarify warning about memory_limit in installer
-* Suppress PHP warning about set_time_limit in installer when safe mode is on
-* (bug 3000) Fall back to SCRIPT_NAME plus QUERY_STRING when REQUEST_URI is
-  not available, as on IIS with PHP-CGI
-* Missing interwiki row for English Wikipedia restored (as "wikipedia:")
-* use configured cache servers for mctest.php
-* bucket details in mcc.php
-* fix input validation and remove debugging code in compressOld
-* full ID range for moveToExternal
-* fix resolveStubs.php for compatibility with older serialized data
-* maximum line length for bar graphs in getLagTimes.php
-* recognize specieswiki in rebuildInterwiki.inc
-* profile unicode cleanup in Xml
-* log slow parses in Article.php
-* profile wfMsgReal
-* log mkdir failures
-* profile AutoLoader
-* rebuild empty DjVu metadata containing ''
-* security fix for DjVu metadata retrieval
-* Undelete page list can use plural marker
-* (bug 8638) Fix update from 1.4 and earlier
-* (bug 8641) Fix order of updates to ipblocks table
-* (bug 8678) Fix detection of self-links for numeric titles in Parser
-* (bug 6171) Magically close tags in tables when not using Tidy.
-* Sanitizer now correctly escapes lonely '>' occurring before the first wikitag.
-* Ignore self closing on closing tags ( '' now gives '') 
-* (bug 8673) Minor fix for web service API content-type header
-* Fix API revision list on PHP 5.2.1; bad reference assignment
-* (bug 8688) Handle underscores/spaces in Special:Blockip and Special:Ipblocklist
-  in a consistent manner
-* (bug 8701) Check database lock status when blocking/unblocking users
-* ParserOptions and ParserOutput classes are now in their own files
-* (bug 8708) Namespace translations for Zealandic language
-* Renamed constructor methods to PHP 5 __construct reserved name
-* (bug 8715) Warn users when editing an interface message whether or not the
-  message page exists
-* ar: fix the 'create a new page' on search page when no exact match found
-* (bug 8703) Corrected talk and image namespace name for Limburgish (li)
-* (bug 8671) Expose "wpDestFile" as a parameter to "uploadtext"
-* (bug 8403) Respect bad image list exceptions in galleries on wiki pages
-* Allow sending per-user contribution requests to "contributions" query group
-* (bug 3717) Update user count for AuthPlugin account autocreation
-* (bug 8719) Firefox release notes lie! Fix tooltips for Firefox 2 on x11;
-  accesskeys default settings appear to be same as Windows.
-* Added an option to make Linker::userToolLinks() show the contribs link
-  red when the user has no edits. Linker::userToolLinksRedContribs() is an
-  alias to that which should be used to make it more self documentating.
-* (bug 8749) Bring MySQL 5 table defs back into sync
-* (bug 8751) Set session cookies to HTTPS-only to match other cookies
-* (bug 8652) Catch exceptions generated by malformed XML in multipage media
-* (bug 8782) Help text in Makefile
-* (bug 8777) Suppress 'previous' link on Special:Allpages when at first page
-* (bug 8774) Fix path for GNU FDL rights icon on new installs
-* Fix multipage selector drop-down for DjVu images to work when title
-  is passed as a query string parameter; we have to pass the title as
-  a form parameter or it gets dropped from the form submission URL
-* (bug 8819) Fix full path disclosure in with skins dependencies
-* Fixed bug affecting HTML formatting in sortable table column titles
-* Merged table sorting code into wikibits.js
-* (bug 8711) Stop floats in previews from spilling into edit area
-* (bug 8858) Safer handling when $wgImageLimits is changed. Added a note
-  in DefaultSettings to make it clear.
-* (bug 4268) Fixed data-loss bug in compressOld batch text compression
-  affecting pages which had null edits (move, protect, etc) as second
-  edit in a batch group. Isolated and patched by Travis Derouin.
-* Fix for paths in 1.4->1.5 special-case updater script
-* (bug 8789) AJAX search: IE users can now use the return key
-* (bug 6844) Use  and  tags to emphase the differences
-* (bug 6684) Fix improper javascript array iteration
-* (bug 4347) use MailAddress object for reply-to
-* Add AlphabeticPager abstract class
-* Use faster AlphabeticPager for Special:Categories
-* (bug 8875) Show printable link in MonoBook sidebar for locally nonexistent
-  pages; perhaps useful for categories and shared images
-* Clean up session checks to better handle the case where the session was
-  opened during the current request. May help with some caching corner
-  cases.
-* (bug 8897) Fix whitespace removal for interlanguage links with link prefix
-* Add 'ParserTestTables' hook to expand the list of temporary tables copied
-  by the parser test harness; use for extensions which require the presence
-  of other tables while they work.
-* Message names changed for AlphabeticPager introduced with r19758
-  for better localisations.
-* (bug 8944) The deprecated is_a() function is used in StubObjects.php
-* (bug 8992) Fix a remaining raw use of REQUEST_URI in history
-* (bug 8999) User.php gives "undefined user editcount" PHP notice.
-* (bug 8984) Fix a database error in Special:Recentchangeslinked
-  when using the Postgres database.
-* Moved the main ob_start() from the default LocalSettings.php to WebStart.php.
-  The ob_start() section should preferably be removed from older
-  LocalSettings.php files.
-* Give Content-Length header for HTTP/1.0 clients.
-* Partial support for Flash cross-domain-policy filtering.
-* Lazy-initialize site_stats row on load when empty. Somewhat kinder to
-  dump-based installations, avoiding PHP warnings when NUMBEROFARTICLES
-  and such are used.
-* Add 'charset' to Content-Type headers on various HTTP error responses
-  to forestall additional UTF-7-autodetect XSS issues. PHP sends only
-  'text/html' by default when the script didn't specify more details,
-  which some inconsiderate browsers consider a license to autodetect
-  the deadly, hard-to-escape UTF-7.
-    This fixes an issue with the Ajax interface error message on MSIE when
-  $wgUseAjax is enabled (not default configuration); this UTF-7 variant
-  on a previously fixed attack vector was discovered by Moshe BA from BugSec:
-  http://www.bugsec.com/articles.php?Security=24
-* Trackback responses now specify XML content type
-* (bug 9044) Send a comment with action=raw pages in CSS/JS output mode
-  to work around IE/Mac bug where empty pages time out verrrrryyyyy slowly,
-  particularly with new keepalive-friendly HTTP on Wikipedia
-* (bug 8919) Suppress paging links and related messages where there are no
-  rows to list for query pages
-* (bug 9057) Standardize MediaWiki: namespace for oc
-* (bug 8132) Suppress "Pages in this category" heading in categories when
-  there are none
-* (bug 8958) Handle search operators better when using tsearch2 (Postgres)
-* (bug 8799) Use redirect table for Special:BrokenRedirects and
-  Special:DoubleRedirects
-* (bug 8918) Enable PLURAL option for MediaWiki:showingresults and
-  MediaWiki:showingresultsnum
-* (bug 9122) Fix minor display issue in RTL with section edit link margin
-* (bug 5805) Enable PLURAL option for some messages of watchlist and statistic
-* (bug 3953) Work around poor display of parenthesis in the in other
-  languages section of MonoBook skin
-* (bug 8539) Enable PLURAL option for another message of recentchanges.
-* (bug 8728) MediaWiki:Badfiletype splitted into 3 messages
-* (bug 9131) Allow SpecialContributions to work with Postgres
-* (bug 9155) Allow footer info to wrap in Monobook
-* (bug 8847) Strip spurious #fragments from request URI to fix redirect
-  loops on some server configurations
-* (bug 9097) column "pr_pagetype" does not exist
-* (bug 9217) Balance wfProfile calls in Skin::outputPage
-* (bug 9222) PostgreSQL updater should not be version-specific
-* Fix fallback implementation of mb_strlen so it works and isn't insanely
-  slow for large strings, since it's used for page edit lengths
-* (bug 8815) Setting password in initUser() breaks LdapAuthentication plugin
-* (bug 9256) Add a quick note to index.php header comments
-* Make Special:Listusers caseinsensitive for first letter
-* Default tidy.conf has been moved from extensions module into includes.
-* Ignore lonely '''''
-* (bug 9244) When calling edit page for nonexistent section, generate error
-  inside of just discarding edits, since edit links sometimes go to the wrong
-  place.
-* (bug 9019) No warning during upload if image description page exists, but no
-  image
-* (bug 8582) Allow thumbnailing when imagesize has a space.
-* (bug 8716) Change math_inputhash and math_outputhash to byte for Postgres
-* (bug 9343) Correct internal name for Wolof language
-* (bug 9363) Fix Postgres error on Recentchangeslinked
-* (bug 5142) Fixed call of hook ArticleViewHeader
-* (bug 4777) Separate prev/next messages for Special:Whatlinkshere
-* Merge approx 15 missing Wikipedia language codes into wikipedia-interwiki.sql
-  based on Jeff Merkey's mediawiki-1.9.3.WG-20070316.tar.gz.bz2 archive.
-* (bug 9411) Fix for shared image descriptions using query-string titles
-* (bug 4756) Add user tool links for self created accounts at special:log
-  instead of sometimes broken block links from newuserlog extension
-* (bug 5817) Special:Recentchangeslinked now shows red link for nonexistent
-  target page instead of silently redirecting
-* (bug 8914) Don't transform colons in {{anchorencode:}}
-* (bug 9241) Handle edit section links and include size links for cached
-  templates the same as the first transclusion.
-* (bug 9466) "Rollback failed" page doesn't format edit comment
-* (bug 9472) Invalid XHTML on cached special pages
-* (bug 9472) Invalid XHTML on Special:Newpages
-* (bug 4764) "My contributions" not bold when viewing own contributions
-* (bug 9194) Add {{PLURAL:...}} to navigation bar of Special:Whatlinkshere
-* (bug 9033) Use a more specific error message when users are not able/allowed
-  to edit page protection levels due to a block, database lock or permissions
-* Fixed $wgFeedLimit
-* (bug 9270) Corrected help namespace name for Dutch Lower Saxon (nds-nl)
-* (bug 929, 4215) Expose "rcdays" user preference in Special:Preferences
-* (bug 9554) Extension-provided group name messages not used
-* (bug 9565) Translate template namespace name for Hindi (hi)
-* (bug 8599) Correct localized names of zh-variants
-* (bug 3366) Require skins based on SkinTemplate to override the skinname
-  property.
-* (bug 9220) Removed obsoletes functions in install-utils.inc.
-* Removed obsoletes Title::getRelatedCache and Title:touchArray
-* (bug 7285) Check MySQL username length during install
-* (bug 6910) Correct date/time formats in Vietnamese (vi)
-* (bug 9608) Correctly use ORDER BY in dumpLinks.php
-* (bug 9609) Correctly use ORDER BY in SpecialWhatlinkshere.php
-* Special:Random and Special:Randomredirect now try harder to send the user to
-  a random page, and will give an error message if none really can be found
-  instead of sending the user to the main page like they used to
-* Fix object variable used for displaying "not-patrolled" CSS class on list
-* Fixed interaction of page parameter to ImagePage with the HTML file cache
-* Fixed MIME type for SVG files, will be silently changed from image/svg 
-  to image/svg+xml after loading from the database.
-* Workaround for djvutoxml bug #1704049 (poor performance). Use djvudump 
-  instead.
-* Fixed odd behaviour in ImagePage on DjVu thumbnailing errors
-* (bug 5439) "Go" title search will now jump to shared/foreign Image: and
-  MediaWiki: pages that have not been locally edited.
-* (bug 9630) Limits links in Whatlinkshere forgot about namespace filter
-* Fixed upgrade for the non-standard MySQL schemas
-* Disable MySQL's strict mode at session start for MySQL 4.1+, to avoid the 
-  various problems that occur when it is on.
-* (bug 9585) Fix regression in tidy usage in Special:Undelete previews
-* (bug 3826) Normalize some invalid cookie name characters when setting
-  up $wgCookiePrefix. Completes application of patch by Anders Kaseorg.
-* (bug 9649) Fix RTL form alignment for Special:Movepage
-* (bug 9582) Members of bot group now mark edits patrolled by default
-* (bug 9669) Fix limit ordering for rebuildrecentchanges; broken since
-  converted from 1.4 to 1.5 schema
-* (bug 9682) Revert PHP 5.1 dependency on warning suppression for SVN info
-* (bug 5959) Anchors dropped from stub links
-* (bug 3348) Some additional weak password checks: password which is same
-  as username will now be rejected.
-* (bug 8602) Converted Special:Contributions to use an IndexPager. The 
-  interpretation of the offset parameter has changed, and the go parameter 
-  has been removed.
-* (bug 7629) Fix $wgBrowserBlackList to avoid false positive on MSIE
-  when certain plugins are present which alter the user agent
-
-
-== Maintenance ==
-
-* New script maintenance/language/checkExtensioni18n.php used to check i18n
-  progress in the extension repository.
-* Running maintenance/parserTests.php with '--record' option, will now
-  automatically attempt to create the required tables
-* --purge option to do additional parser-cache purging for purgeList.php
-* Fix hardcoded background color in parserTests.php
-* parserTests.php : removed the 'light' option for --color argument, replacing
-  it with a new global switch : $wgCommandLineDarkBg
-* (bug 8780) Clarify message for command-line scripts if LocalSettings.php
-  exists but is not readable
-* dumpBackup / importDump now work with PostgreSQL
-* (bug 8975) Use "Maintenance script" as the default username for importImages.php
-  and importTextFile.php scripts
-* (bug 8933) Fix maintenance/reassignEdits.php script
-* (bug 9440) Added "mediawikiwiki" interwiki prefix to MediaWiki.org
-* (bug 2979) Import now gracefully skips invalid titles with a warning
-* Restore '--norc' option for maintenance/importTextFile.php
-* Help information for maintenance/importTextFile.php now easier to read on
-  consoles
-* Doxygen documentation now show the revision number of each file, generate
-  graphs using dot and include a search engine.
-
-
-== Languages updated ==
-
+* 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)
-* Aramaic (arc)
-* Aymara (ay)
-* Belarusian normative (be)
-* Belarusian alternative (be-x-old)
+* Bikol (bcl)
 * Bulgarian (bg)
-* Bihara (bh)
-* Breton (br)
 * Catalan (ca)
-* Czech (cs)
 * Danish (da)
 * German (de)
 * Greek (el)
 * Esperanto (eo)
 * Spanish (es)
 * Estonian (et)
-* Basque (eu)
+* Extremaduran (ext)
+* Farsi (fa)
 * Finnish (fi)
-* Võro (fiu-vro)
+* Vöro (fiu-vro)
 * French (fr)
+* Français Cadien (frc) (new)
+* Franco-Provençal/Arpetan (frp)
+* Galician (gl)
+* Hakka (hak)
 * Hebrew (he)
-* Hindi (hi)
 * Upper Sorbian (hsb)
-* Hungarian (hu)
-* Armenian (hy)
+* Haitian (ht)
 * Indonesian (id)
+* Icelandic (is)
 * Italian (it)
 * Japanese (ja)
-* Javanese (jv)
 * Georgian (ka)
 * Kabyle (kab)
 * Kazakh (kk)
 * Korean (ko)
-* Kashmiri (ks)
-* Ripuarian (ksh)
+* Kinaray-a (krj) (new)
+* Kurdish (ku)
 * Latin (la)
-* Luganda (lg)
-* Limburgish (li)
+* Lao (lo)
 * Lithuanian (lt)
-* Latvian (lv)
-* Marathi (mr)
-* Low Saxon (nds)
-* Dutch Lower Saxon (nds-nl)
-* Nepali (ne)
-* Nepal Bhasa (new)
+* Latviešu (lv)
+* Malayalam (ml)
+* Bahasa Melayu (ms)
+* Burmese (my)
+* Low German (nds)
 * Dutch (nl)
+* Norwegian (no)
 * Occitan (oc)
-* Pali (pi)
+* Punjabi (Gurmukhi) (pa)
 * Polish (pl)
+* Piedmontese (pms)
+* Portuguese (pt)
+* Romani (rmy)
 * Romanian (ro)
+* Aromanian (roa-rup)
 * Russian (ru)
-* Sanskrit (sa)
-* Sicilian (scn)
+* Sakha (sah)
+* Sango (se) (new)
 * Slovak (sk)
+* Slovenian (sl)
+* Shona (sn)
+* Somali (so)
+* Albanian (sq)
 * Sundanese (su)
 * Swedish (sv)
-* Tahitian (ty)
-* Ukrainian (uk)
-* Urdu (ur)
-* Uzbek (uz)
-* Vietnamese (vi)
-* Zealandic (zea)
+* 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)
@@ -550,7 +625,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
 
 == Compatibility ==
 
-MediaWiki 1.10 requires PHP 5 (5.1 recommended). PHP 4 is no longer supported.
+MediaWiki 1.11 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
@@ -562,7 +637,7 @@ At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases.
 
 == Upgrading ==
 
-1.10 has several database changes since 1.9, and will not work without schema
+1.11 has several database changes since 1.10, and will not work without schema
 updates.
 
 If upgrading from before 1.7, you may want to run refreshLinks.php to ensure
@@ -583,7 +658,7 @@ cases, but this is not recommended on live sites. (This must be set for
 MathML to display properly in Mozilla.)
 
 
-For notes on 1.9.x and older releases, see HISTORY.
+For notes on 1.10.x and older releases, see HISTORY.
 
 
 === Online documentation ===
diff --git a/StartProfiler.php b/StartProfiler.php
index 8fc3ff88..3fcf69e6 100644
--- a/StartProfiler.php
+++ b/StartProfiler.php
@@ -19,4 +19,4 @@ require_once( dirname(__FILE__).'/includes/ProfilerStub.php' );
  * Configuration of the profiler output can be done in LocalSettings.php
  */
 
-?>
+
diff --git a/Test.php b/Test.php
index bad931a4..d6a2cf91 100644
--- a/Test.php
+++ b/Test.php
@@ -1,304 +1,495 @@
  for PHP
-
-=head1 SYNOPSIS
-
-  require 'Test.php';
-
-  plan( $num ); # plan $num tests
-  # or
-  plan( 'no_plan' ); # We don't know how many
-  # or
-  plan( 'skip_all' ); # Skip all tests
-  # or
-  plan( 'skip_all', $reason ); # Skip all tests with a reason
-
-  diag( 'message in test output' ) # Trailing \n not required
-
-  # $test_name is always optional and should be a short description of
-  # the test, e.g. "some_function() returns an integer"
+# The latest release of this test framework can always be found on CPAN:
+# http://search.cpan.org/search?query=Test.php
 
-  # Various ways to say "ok"
-  ok( $got == $expected, $test_name );
-  
-  # Compare with == and !=
-  is( $got, $expected, $test_name );
-  isnt( $got, $expected, $test_name );
-
-  # Run a preg match on some data
-  like( $got, $regex, $test_name );
-  unlike( $got, $regex, $test_name );
-
-  # Compare something with a given comparison operator
-  cmp_ok( $got, '==', $expected, $test_name );
-  # Compare something with a comparison function (should return bool)
-  cmp_ok( $got, $func, $expected, $test_name );
-
-  # Recursively check datastructures for equalness
-  is_deeply( $got, $expected, $test_name );
-
-  # Always pass or fail a test under an optional name
-  pass( $test_name );
-  fail( $test_name );
+register_shutdown_function('_test_ends');
 
-=head1 DESCRIPTION
+$__Test = array(
+    # How many tests are planned
+    'planned'   => null,
 
-F is an implementation of Perl's L and Pugs's B for
-PHP. Like those two modules it produces TAP output (see L) which
-can then be gathered, formatted and summarized by a program that
-understands TAP such as L.
+    # How many tests we've run, if 'planned' is still null by the time we're
+    # done we report the total count at the end
+    'run' => 0,
 
-=cut
-
-*/
-
-register_shutdown_function('test_ends');
-
-$Test = array(
-	  'run'       => 0,
-	  'failed'    => 0,
-	  'badpass'   => 0,
-	  'planned'   => null
+    # Are are we currently within todo_start()/todo_end() ?
+    'todo' => array(),
 );
 
-function plan( $plan, $why = '' )
+function plan($plan, $why = '')
 {
-	global $Test;
-
-	$Test['planned'] = true;
-
-	switch ( $plan )
-	{
-	case 'no_plan':
-		$Test['planned'] = false;
-		break;
-	case 'skip_all';
-		printf( "1..0%s\n", $why ? " # Skip $why" : '' );
-		exit;
-	default:
-		printf( "1..%d\n", $plan );
-		break;
-	}
+    global $__Test;
+
+    $__Test['planned'] = true;
+
+    switch ($plan)
+    {
+      case 'no_plan':
+        $__Test['planned'] = false;
+        break;
+      case 'skip_all';
+        printf("1..0%s\n", $why ? " # Skip $why" : '');
+        exit;
+      default:
+        printf("1..%d\n", $plan);
+        break;
+    }
 }
 
-function pass( $desc = '' )
+function pass($desc = '')
 {
-	return proclaim(true, $desc);
+    return _proclaim(true, $desc);
 }
 
-function fail( $desc = '' )
+function fail($desc = '')
 {
-	return proclaim( false, $desc );
+    return _proclaim(false, $desc);
 }
 
-function ok( $cond, $desc = '' ) {
-	return proclaim( $cond, $desc );
+function ok($cond, $desc = '') {
+    return _proclaim($cond, $desc);
 }
 
-function is( $got, $expected, $desc = '' ) {
-	$pass = $got == $expected;
-	return proclaim( $pass, $desc, /* todo */ false, $got, $expected );
+function is($got, $expected, $desc = '') {
+    $pass = $got == $expected;
+    return _proclaim($pass, $desc, /* todo */ false, $got, $expected);
 }
 
-function isnt( $got, $expected, $desc = '' ) {
-	$pass = $got != $expected;
-	return proclaim( $pass, $desc, /* todo */ false, $got, $expected, /* negated */ true );
+function isnt($got, $expected, $desc = '') {
+    $pass = $got != $expected;
+    return _proclaim($pass, $desc, /* todo */ false, $got, $expected, /* negated */ true);
 }
 
-function like( $got, $expected, $desc = '' ) {
-	$pass = preg_match( $expected, $got );
-	return proclaim( $pass, $desc,  /* todo */ false, $got, $expected );
+function like($got, $expected, $desc = '') {
+    $pass = preg_match($expected, $got);
+    return _proclaim($pass, $desc, /* todo */ false, $got, $expected);
 }
 
-function unlike( $got, $expected, $desc = '' ) {
-	$pass = ! preg_match( $expected, $got );
-	return proclaim( $pass, $desc,  /* todo */ false, $got, $expected, /* negated */ true );
+function unlike($got, $expected, $desc = '') {
+    $pass = !preg_match($expected, $got);
+    return _proclaim($pass, $desc, /* todo */ false, $got, $expected, /* negated */ true);
 }
 
 function cmp_ok($got, $op, $expected, $desc = '')
 {
-	$pass = null;
-
-	/* See http://www.php.net/manual/en/language.operators.comparison.php */
-	switch ($op)
-	{
-	case '==':
-		$pass = $got == $expected;
-		break;
-	case '===':
-		$pass = $got === $expected;
-		break;
-	case '!=':
-	case '<>':
-		$pass = $got != $expected;
-		break;
-	case '!==':
-		$pass = $got !== $expected;
-		break;
-	case '<':
-		$pass = $got < $expected;
-		break;
-	case '>':
-		$pass = $got > $expected;
-		break;
-	case '<=':
-		$pass = $got <= $expected;
-		break;
-	case '>=':
-		$pass = $got >= $expected;
-		break;
-	default:
-		if ( function_exists( $op ) ) {
-			$pass = $op( $got, $expected );
-		} else {
-			die("No such operator or function $op\n");
-		}
-	}
-
-	return proclaim( $pass, $desc, /* todo */ false, $got, "$op $expected" );
+    $pass = null;
+
+    # See http://www.php.net/manual/en/language.operators.comparison.php
+    switch ($op)
+    {
+      case '==':
+        $pass = $got == $expected;
+        break;
+      case '===':
+        $pass = $got === $expected;
+        break;
+      case '!=':
+      case '<>':
+        $pass = $got != $expected;
+        break;
+      case '!==':
+        $pass = $got !== $expected;
+        break;
+      case '<':
+        $pass = $got < $expected;
+        break;
+      case '>':
+        $pass = $got > $expected;
+        break;
+      case '<=':
+        $pass = $got <= $expected;
+        break;
+      case '>=':
+        $pass = $got >= $expected;
+        break;
+    default:
+        if (function_exists($op)) {
+            $pass = $op($got, $expected);
+        } else {
+            die("No such operator or function $op\n");
+        }
+    }
+
+    return _proclaim($pass, $desc, /* todo */ false, $got, "$got $op $expected");
 }
 
 function diag($message)
 {
     if (is_array($message))
-	{
-	    $message = implode("\n", $message);
-	}
-
-    $messages = explode("\n", $message);
+    {
+        $message = implode("\n", $message);
+    }
 
-    foreach ($messages as $msg)
-	{
-        echo "# $msg\n";
+    foreach (explode("\n", $message) as $line)
+    {
+        echo "# $line\n";
     }
 }
 
-function include_ok( $file, $desc = '' )
+function include_ok($file, $desc = '')
 {
     $pass = include $file;
-    return proclaim( $pass, $desc == '' ? "include $file" : $desc );
+    return _proclaim($pass, $desc == '' ? "include $file" : $desc);
 }
 
-function require_ok( $file, $desc = '' )
+function require_ok($file, $desc = '')
 {
     $pass = require $file;
-    return proclaim( $pass, $desc == '' ? "require $file" : $desc );
+    return _proclaim($pass, $desc == '' ? "require $file" : $desc);
 } 
 
-function is_deeply( $got, $expected, $desc = '' )
+function is_deeply($got, $expected, $desc = '')
 {
-    // hack
-    $s_got = serialize( $got );
-	$s_exp = serialize( $expected );
+    $diff = _cmp_deeply($got, $expected);
+    $pass = is_null($diff);
+
+    if (!$pass) {
+        $got      = strlen($diff['gpath']) ? ($diff['gpath'] . ' = ' . $diff['got']) 
+                                           : _repl($got);
+        $expected = strlen($diff['epath']) ? ($diff['epath'] . ' = ' . $diff['expected']) 
+                                           : _repl($expected);
+    }
 
-	$pass = $s_got == $s_exp;
+    _proclaim($pass, $desc, /* todo */ false, $got, $expected);
+}
 
-	proclaim( $pass, $desc, /* todo */ false, $got, $expected );
+function isa_ok($obj, $expected, $desc = '')
+{
+    $pass = is_a($obj, $expected);
+    _proclaim($pass, $desc, /* todo */ false, $name, $expected);
 }
 
-function isa_ok( $obj, $expected, $desc = '' ) {
-	$name = get_class( $obj );
-	$pass = $name == $expected;
-	proclaim( $pass, $desc, /* todo */ false, $name, $expected );
-} 
+function todo_start($why = '')
+{
+    global $__Test;
 
-function proclaim(
-	$cond, // bool
-	$desc = '',
-	$todo = false,
-	$got = null,
-	$expected = null,
-	$negate = false ) {
+    $__Test['todo'][] = $why;
+}
 
-	global $Test;
+function todo_end()
+{
+    global $__Test;
 
-	$Test['run'] += 1;
+    if (count($__Test['todo']) == 0) {
+        die("todo_end() called without a matching todo_start() call");
+    } else {
+        array_pop($__Test['todo']);
+    }
+}
+
+#
+# The code below consists of private utility functions for the above functions
+#
 
-	# TODO: force_todo
+function _proclaim(
+    $cond, # bool
+    $desc = '',
+    $todo = false,
+    $got = null,
+    $expected = null,
+    $negate = false) {
 
-	# Everything after the first # is special, so escape user-supplied messages
-	$desc = str_replace( '#', '\\#', $desc );
-	$desc = str_replace( "\n", '\\n', $desc );
+    global $__Test;
 
-	$ok = $cond ? "ok" : "not ok";
-	$directive = $todo === false ? '' : '# TODO aoeu';
+    $__Test['run'] += 1;
 
-	printf( "%s %d %s%s\n", $ok, $Test['run'], $desc, $directive );
+    # We're in a TODO block via todo_start()/todo_end(). TODO via specific
+    # functions is currently unimplemented and will probably stay that way
+    if (count($__Test['todo'])) {
+        $todo = true;
+    }
 
-	if ( ! $cond ) {
-		report_failure( $desc, $got, $expected, $negate, $todo );
-	}
+    # Everything after the first # is special, so escape user-supplied messages
+    $desc = str_replace('#', '\\#', $desc);
+    $desc = str_replace("\n", '\\n', $desc);
 
-	return $cond;
-}
+    $ok = $cond ? "ok" : "not ok";
+    $directive = '';
 
-function report_failure( $desc, $got, $expected, $negate, $todo ) {
-	# Every public function in this file proclaim which then calls
-    #  this function, so our culprit is the third item in the stack
-	$caller = debug_backtrace();
-	$call = $caller['2'];
-
-	diag(
-		sprintf( " Failed%stest '%s'\n in %s at line %d\n       got: %s\n  expected: %s",
-			$todo ? ' TODO ' : ' ',
-			$desc,
-			$call['file'],
-			$call['line'],
-			$got,
-			$expected
-		)
-	);
+    if ($todo) {
+        $todo_idx = count($__Test['todo']) - 1;
+        $directive .= ' # TODO ' . $__Test['todo'][$todo_idx];
+    }
+
+    printf("%s %d %s%s\n", $ok, $__Test['run'], $desc, $directive);
+
+    # report a failure
+    if (!$cond) {
+        # Every public function in this file calls _proclaim so our culprit is
+        # the second item in the stack
+        $caller = debug_backtrace();
+        $call = $caller['1'];
+    
+        diag(
+            sprintf(" Failed%stest '%s'\n in %s at line %d\n       got: %s\n  expected: %s",
+                $todo ? ' TODO ' : ' ',
+                $desc,
+                $call['file'],
+                $call['line'],
+                $got,
+                $expected
+            )
+        );
+    }
+
+    return $cond;
 }
 
-function test_ends ()
+function _test_ends()
 {
-	global $Test;
+    global $__Test;
+
+    if (count($__Test['todo']) != 0) {
+        $todos = join("', '", $__Test['todo']);
+        die("Missing todo_end() for '$todos'");
+    }
 
-	if ( $Test['planned'] === false ) {
-		printf( "1..%d\n", $Test['run'] );
-	}
+    if (!$__Test['planned']) {
+        printf("1..%d\n", $__Test['run']);
+    }
 }
 
-/*
+#
+# All of the below is for is_deeply()
+#
+
+function _repl($obj, $deep = true) {
+    if (is_string($obj)) {
+        return "'" . $obj . "'";
+    } else if (is_numeric($obj)) {
+        return $obj;
+    } else if (is_null($obj)) {
+        return 'null';
+    } else if (is_bool($obj)) {
+        return $obj ? 'true' : 'false';
+    } else if (is_array($obj)) {
+        return _repl_array($obj, $deep);
+    }else {
+        return gettype($obj);
+    }
+}
 
-=head1 TODO
+function _diff($gpath, $got, $epath, $expected) {
+    return array(
+        'gpath'     => $gpath,
+        'got'       => $got,
+        'epath'     => $epath,
+        'expected'  => $expected
+    );
+}
 
-=over
+function _idx($obj, $path = '') {
+    return $path . '[' . _repl($obj) . ']';
+}
 
-=item * Fully document this file
+function _cmp_deeply($got, $exp, $path = '') {
+    if (is_array($exp)) {
+        
+        if (!is_array($got)) {
+            return _diff($path, _repl($got), $path, _repl($exp));
+        }
+        
+        $gk = array_keys($got);
+        $ek = array_keys($exp);
+        $mc = max(count($gk), count($ek));
+
+        for ($el = 0; $el < $mc; $el++) {
+            # One array shorter than the other?
+            if ($el >= count($ek)) {
+                return _diff(_idx($gk[$el], $path), _repl($got[$gk[$el]]), 
+                             'missing', 'nothing');
+            } else if ($el >= count($gk)) {
+                return _diff('missing', 'nothing', 
+                             _idx($ek[$el], $path), _repl($exp[$ek[$el]]));
+            }
+            
+            # Keys differ?
+            if ($gk[$el] != $ek[$el]) {
+                return _diff(_idx($gk[$el], $path), _repl($got[$gk[$el]]), 
+                             _idx($ek[$el], $path), _repl($exp[$ek[$el]]));
+            }
+
+            # Recurse
+            $rc = _cmp_deeply($got[$gk[$el]], $exp[$ek[$el]], _idx($gk[$el], $path));
+            if (!is_null($rc)) {
+                return $rc;
+            }
+        }
+    }
+    else {
+        # Default to serialize hack
+        if (serialize($got) != serialize($exp)) {
+            return _diff($path, _repl($got), $path, _repl($exp));
+        }
+    }
+    
+    return null;
+}
 
-=item *
+function _plural($n, $singular, $plural = null) {
+    if (is_null($plural)) {
+        $plural = $singular . 's';
+    }
+    return $n == 1 ? "$n $singular" : "$n $plural";
+}
 
-Add TODO support, maybe via C
-C 'fix this'))>.
+function _repl_array($obj, $deep) {
+    if ($deep) {
+        $slice = array_slice($obj, 0, 3); # Increase from 3 to show more
+        $repl  = array();
+        $next  = 0;
+        foreach ($slice as $idx => $el) {
+            $elrep = _repl($el, false);
+            if (is_numeric($idx) && $next == $idx) {
+                // Numeric index
+                $next++;
+            } else {
+                // Out of sequence or non-numeric
+                $elrep = _repl($idx, false) . ' => ' . $elrep;
+            }
+            $repl[] = $elrep;
+        }
+        $more = count($obj) - count($slice);
+        if ($more > 0) {
+            $repl[] = '... ' . _plural($more, 'more element')  . ' ...';
+        }
+        return 'array(' . join(', ', $repl) . ')';
+    }
+    else {
+        return 'array(' . count($obj) . ')';
+    }
+}
 
-=back
+/*
 
-=head1 SEE ALSO
+=head1 NAME
 
-=over
+Test.php - TAP test framework for PHP with a L-like interface
 
-=item L - The TAP protocol
+=head1 SYNOPSIS
 
-=item L 
+    #!/usr/bin/env php
+    
+  
+=head1 DESCRIPTION
 
-=item Pugs's Test.pm
+F is an implementation of Perl's L for PHP. Like
+Test::More it produces language agnostic TAP output (see L) which
+can then be gathered, formatted and summarized by a program that
+understands TAP such as prove(1).
+
+=head1 HOWTO
+
+First place the F in the project root or somewhere else in
+the include path where C and C will find it.
+
+Then make a place to put your tests in, it's customary to place TAP
+tests in a directory named F under the root but they can be
+anywhere you like. Make a test in this directory or one of its subdirs
+and try running it with php(1):
+
+    $ php t/pass.t 
+    1..1
+    ok 1 This dummy test passed
+
+The TAP output consists of very simple output, of course reading
+larger output is going to be harder which is where prove(1) comes
+in. prove is a harness program that reads test output and produces
+reports based on it:
+    
+    $ prove t/pass.t 
+    t/pass....ok
+    All tests successful.
+    Files=1, Tests=1,  0 wallclock secs ( 0.03 cusr +  0.02 csys =  0.05 CPU)
+
+To run all the tests in the F directory recursively use C. This can be put in a F under a I target, for
+example:
+
+    test: Test.php
+		prove -r t
+    
+For reference the example test file above looks like this, the shebang
+on the first line is needed so that prove(1) and other test harness
+programs know they're dealing with a PHP file.
+
+    #!/usr/bin/env php
+    
+    
+=head1 SEE ALSO
 
-=back
+L - The TAP protocol
 
 =head1 AUTHOR
 
-Ævar Arnfjörð Bjarmason 
+Evar ArnfjErE Bjarmason  and Andy Armstrong 
 
 =head1 LICENSING
 
-This program is free software; you can redistribute it and/or modify it
-under the same terms as Perl itself.
+The author or authors of this code dedicate any and all copyright
+interest in this code to the public domain. We make this dedication
+for the benefit of the public at large and to the detriment of our
+heirs and successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights this
+code under copyright law.
 
 =cut
 
diff --git a/UPGRADE b/UPGRADE
index 40d5b202..fb600ff2 100644
--- a/UPGRADE
+++ b/UPGRADE
@@ -16,7 +16,7 @@ is available at http://www.mediawiki.org/wiki/Manual:Upgrading_MediaWiki.
 === Consult the release notes ===
 
 Before doing anything, stop and consult the release notes supplied with the new
-version of the software. This detail bug fixes, new features and functionality,
+version of the software. These detail bug fixes, new features and functionality,
 and any particular points that may need to be noted during the upgrade
 procedure.
 
@@ -27,32 +27,34 @@ you take a complete backup of your wiki database and files and verify it. While
 the upgrade scripts are somewhat robust, there is no guarantee that things will
 not fail, leaving the database in an inconsistent state.
 
-Refer to the MySQL or Postgres documentation for information on backing up a 
-database. For information on making copies of files, consult the documentation 
-for your operating system.
+http://www.mediawiki.org/wiki/Manual:Backing_up_a_wiki provides an overview of
+the upgrade process. You should also refer to the documentation for your
+database management system for information on backing up a database, and to
+your operating system documentation for information on making copies of files.
 
 === Perform the file upgrade ===
 
-Having downloaded the desired new version of the software, either as a package
-from SourceForge, or via an export from Subversion, decompress the files as
-needed, and replace the existing MediaWiki files with the new.
+Download the files for the new version of the software. These are available
+as a compressed "tar" archive from the Wikimedia Download Service
+(http://download.wikimedia.org/mediawiki).
 
-You should preserve:
+You can also obtain the new files directly from our Subversion source code
+repository, via a checkout or export operation.
 
-* The LocalSettings.php file
-* The AdminSettings.php file, where it exists
-* The extensions directory
-* The images directory
+Replace the existing MediaWiki files with the new. You should preserve the
+LocalSettings.php file, AdminSettings.php file (if present), and the
+"extensions" and "images" directories.
 
-If using an alternative uploads directory, preserve this; and if using custom
-skins, preserve these too. The core code is now updated.
+Depending upon your configuration, you may also need to preserve additional
+directories, including a custom upload directory ($wgUploadDirectory),
+deleted file archives, and any custom skins.
 
 === Perform the database upgrade ===
 
 You will need an AdminSettings.php file set up in the correct format; see
 AdminSettings.sample in the wiki root for more information and examples.
 
-From the command line, browse to the maintenance directory and run the 
+From the command line, browse to the "maintenance" directory and run the 
 update.php script to check and update the schema. This will insert missing
 tables, update existing tables, and move data around as needed. In most cases,
 this is successful and nothing further needs to be done.
@@ -72,6 +74,8 @@ procedure, and especially after upgrading; check that page views and edits work
 normally and that special pages continue to function, etc. and correct errors
 and quirks which reveal themselves.
 
+You should also test any extensions, and upgrade these if necessary.
+
 == Upgrading from 1.8 wikis ==
 
 MediaWiki 1.9 and later no longer keep default localized message text
diff --git a/api.php b/api.php
index d3274dc4..fa85573d 100644
--- a/api.php
+++ b/api.php
@@ -1,10 +1,9 @@
 
+* Copyright (C) 2006 Yuri Astrakhan @gmail.com
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
@@ -22,6 +21,17 @@
 * http://www.gnu.org/copyleft/gpl.html
 */
 
+/** 
+ * This file is the entry point for all API queries. It begins by checking 
+ * whether the API is enabled on this wiki; if not, it informs the user that
+ * s/he should set $wgEnableAPI to true and exits. Otherwise, it constructs
+ * a new ApiMain using the parameter passed to it as an argument in the URL
+ * ('?action=') and with write-enabled set to the value of $wgEnableWriteAPI
+ * as specified in LocalSettings.php. It then invokes "execute()" on the
+ * ApiMain object instance, which produces output in the format sepecified
+ * in the URL.
+ */
+
 // Initialise common code
 require (dirname(__FILE__) . '/includes/WebStart.php');
 
@@ -34,9 +44,16 @@ if (!$wgEnableAPI) {
 	die(-1);
 }
 
+/* Construct an ApiMain with the arguments passed via the URL. What we get back
+ * is some form of an ApiMain, possibly even one that produces an error message,
+ * but we don't care here, as that is handled by the ctor.
+ */
 $processor = new ApiMain($wgRequest, $wgEnableWriteAPI);
+
+// Process data & print results
 $processor->execute();
 
+// Log what the user did, for book-keeping purposes.
 wfProfileOut('api.php');
 wfLogProfilingData();
-?>
+
diff --git a/api.php5 b/api.php5
new file mode 100644
index 00000000..64088872
--- /dev/null
+++ b/api.php5
@@ -0,0 +1 @@
+
diff --git a/config/index.php b/config/index.php
index e719e47e..274a1531 100644
--- a/config/index.php
+++ b/config/index.php
@@ -29,10 +29,6 @@ $wgRequestTime = microtime( true );
 # Attempt to set up the include path, to fix problems with relative includes
 $IP = dirname( dirname( __FILE__ ) );
 define( 'MW_INSTALL_PATH', $IP );
-$sep = PATH_SEPARATOR;
-if( !ini_set( "include_path", ".$sep$IP$sep$IP/includes$sep$IP/languages" ) ) {
-	set_include_path( ".$sep$IP$sep$IP/includes$sep$IP/languages" );
-}
 
 # Define an entry point and include some files
 define( "MEDIAWIKI", true );
@@ -40,14 +36,21 @@ define( "MEDIAWIKI_INSTALL", true );
 
 // Run version checks before including other files
 // so people don't see a scary parse error.
-require_once( "install-utils.inc" );
+require_once( "$IP/install-utils.inc" );
 install_version_checks();
 
-require_once( "includes/Defines.php" );
-require_once( "includes/DefaultSettings.php" );
-require_once( "includes/MagicWord.php" );
-require_once( "includes/Namespace.php" );
-require_once( "includes/ProfilerStub.php" );
+require_once( "$IP/includes/Defines.php" );
+require_once( "$IP/includes/DefaultSettings.php" );
+require_once( "$IP/includes/AutoLoader.php" );
+require_once( "$IP/includes/MagicWord.php" );
+require_once( "$IP/includes/Namespace.php" );
+require_once( "$IP/includes/ProfilerStub.php" );
+require_once( "$IP/includes/GlobalFunctions.php" );
+require_once( "$IP/includes/Hooks.php" );
+
+# If we get an exception, the user needs to know
+# all the details
+$wgShowExceptionDetails = true;
 
 ## Databases we support:
 
@@ -146,6 +149,15 @@ $ourdb['postgres']['rootuser']   = 'postgres';
 			font-size: 85%;
 			padding-top: 3em;
 		}
+		
+		span.success-message {
+			font-weight: bold;
+			font-size: 110%;
+			color: green;
+		}
+		.success-box {
+			font-size: 130%;
+		}
 
 	
 	" );
+	}
 
 	/**
-	 * Get diff table, including header
-	 * Note that the interface has changed, it's no longer static.
-	 * Returns false on error
+	 * Get complete diff table, including header
+	 *
+	 * @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 ) {
-		$body = $this->getDiffBody();
+	function getDiff( $otitle, $ntitle, $skipCache = false ) {
+		$body = $this->getDiffBody( $skipCache );
 		if ( $body === false ) {
 			return false;
 		} else {
@@ -310,19 +367,20 @@ CONTROL;
 
 	/**
 	 * Get the diff table body, without header
-	 * Results are cached
-	 * Returns false on error
+	 *
+	 * @param bool $skipCache Skip cache for this request?
+	 * @return mixed
 	 */
-	function getDiffBody() {
+	function getDiffBody( $skipCache = false ) {
 		global $wgMemc;
 		$fname = 'DifferenceEngine::getDiffBody';
 		wfProfileIn( $fname );
 		
 		// Cacheable?
 		$key = false;
-		if ( $this->mOldid && $this->mNewid ) {
+		if ( $this->mOldid && $this->mNewid && !$skipCache ) {
 			// Try cache
-			$key = wfMemcKey( 'diff', 'oldid', $this->mOldid, 'newid', $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' );
@@ -488,10 +546,14 @@ CONTROL;
 		   $ntitle = ''.$ntitle.'';
 		}
 		$header = "
-			
+			
+ + + + - - + + "; @@ -530,16 +592,15 @@ CONTROL; } // Load the new revision object - if( $this->mNewid ) { - $this->mNewRev = Revision::newFromId( $this->mNewid ); - } else { - $this->mNewRev = Revision::newFromTitle( $this->mTitle ); - } - - if( is_null( $this->mNewRev ) ) { + $this->mNewRev = $this->mNewid + ? Revision::newFromId( $this->mNewid ) + : Revision::newFromTitle( $this->mTitle ); + if( !$this->mNewRev instanceof Revision ) return false; - } - + + // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) + $this->mNewid = $this->mNewRev->getId(); + // Set assorted variables $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true ); $this->mNewPage = $this->mNewRev->getTitle(); @@ -588,7 +649,8 @@ CONTROL; $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid ); $this->mOldtitle = "" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) . " (" . htmlspecialchars( wfMsg( 'editold' ) ) . ")"; - //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo) + + // Add an "undo" link $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid); $this->mNewtitle .= " (" . htmlspecialchars( wfMsg( 'editundo' ) ) . ")"; } @@ -1721,8 +1783,8 @@ class TableDiffFormatter extends DiffFormatter } function _block_header( $xbeg, $xlen, $ybeg, $ylen ) { - $r = '\n" . - '\n"; + $r = '\n" . + '\n"; return $r; } @@ -1738,17 +1800,25 @@ class TableDiffFormatter extends DiffFormatter # HTML-escape parameter before calling this function addedLine( $line ) { - return ""; + return $this->wrapLine( '+', 'diff-addedline', $line ); } # HTML-escape parameter before calling this function deletedLine( $line ) { - return ""; + return $this->wrapLine( '-', 'diff-deletedline', $line ); } # HTML-escape parameter before calling this function contextLine( $line ) { - return ""; + return $this->wrapLine( ' ', 'diff-context', $line ); + } + + private function wrapLine( $marker, $class, $line ) { + if( $line !== '' ) { + // The
wrapper is needed for 'overflow: auto' style to scroll properly + $line = "
$line
"; + } + return "
"; } function emptyLine() { @@ -1801,4 +1871,5 @@ class TableDiffFormatter extends DiffFormatter } } -?> + + diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index 1e423565..b48aaffd 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -104,7 +104,9 @@ class DjVuImage { } function getInfo() { + wfSuppressWarnings(); $file = fopen( $this->mFilename, 'rb' ); + wfRestoreWarnings(); if( $file === false ) { wfDebug( __METHOD__ . ": missing or failed file read\n" ); return false; diff --git a/includes/EditPage.php b/includes/EditPage.php index bec6e300..cceb053d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -20,6 +20,7 @@ class EditPage { var $firsttime; var $lastDelete; var $mTokenOk = false; + var $mTokenOkExceptSuffix = false; var $mTriedSave = false; var $tooBig = false; var $kblength = false; @@ -97,6 +98,11 @@ class EditPage { $text = $this->mArticle->getContent(); + if ($undo > 0 && $undoafter > 0 && $undo < $undoafter) { + # If they got undoafter and undo round the wrong way, switch them + list( $undo, $undoafter ) = array( $undoafter, $undo ); + } + if ( $undo > 0 && $undo > $undoafter ) { # Undoing a specific edit overrides section editing; section-editing # doesn't work with undoing. @@ -292,7 +298,6 @@ class EditPage { */ function edit() { global $wgOut, $wgUser, $wgRequest, $wgTitle; - global $wgEmailConfirmToEdit; if ( ! wfRunHooks( 'AlternateEdit', array( &$this ) ) ) return; @@ -313,57 +318,38 @@ class EditPage { return; } - if ( ! $this->mTitle->userCan( 'edit' ) ) { - wfDebug( "$fname: user can't edit\n" ); - $wgOut->readOnlyPage( $this->getContent(), true ); - wfProfileOut( $fname ); - return; - } - wfDebug( "$fname: Checking blocks\n" ); - if ( !$this->preview && !$this->diff && $wgUser->isBlockedFrom( $this->mTitle, !$this->save ) ) { - # When previewing, don't check blocked state - will get caught at save time. - # Also, check when starting edition is done against slave to improve performance. - wfDebug( "$fname: user is blocked\n" ); - $this->blockedPage(); - wfProfileOut( $fname ); - return; - } - if ( !$wgUser->isAllowed('edit') ) { - if ( $wgUser->isAnon() ) { - wfDebug( "$fname: user must log in\n" ); - $this->userNotLoggedInPage(); - wfProfileOut( $fname ); - return; - } else { - wfDebug( "$fname: read-only page\n" ); - $wgOut->readOnlyPage( $this->getContent(), true ); - wfProfileOut( $fname ); - return; + $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser); + if( !$this->mTitle->exists() ) + $permErrors += $this->mTitle->getUserPermissionsErrors( 'create', $wgUser); + + # Ignore some permissions errors. + $remove = array(); + foreach( $permErrors as $error ) { + if ($this->preview || $this->diff && + ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext')) + { + // Don't worry about blocks when previewing/diffing + $remove[] = $error; + } + + if ($error[0] == 'readonlytext') + { + if ($this->edit) { + $this->formtype = 'preview'; + } elseif ($this->save || $this->preview || $this->diff) { + $remove[] = $error; + } } } - if ($wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed()) { - wfDebug("$fname: user must confirm e-mail address\n"); - $this->userNotConfirmedPage(); - wfProfileOut($fname); - return; - } - if ( !$this->mTitle->userCan( 'create' ) && !$this->mTitle->exists() ) { - wfDebug( "$fname: no create permission\n" ); - $this->noCreatePermission(); + # 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" ); + $wgOut->readOnlyPage( $this->getContent(), true, $permErrors ); wfProfileOut( $fname ); return; - } - if ( wfReadOnly() ) { - wfDebug( "$fname: read-only mode is engaged\n" ); - if( $this->save || $this->preview ) { - $this->formtype = 'preview'; - } else if ( $this->diff ) { - $this->formtype = 'diff'; - } else { - $wgOut->readOnlyPage( $this->getContent() ); - wfProfileOut( $fname ); - return; - } } else { if ( $this->save ) { $this->formtype = 'save'; @@ -410,9 +396,10 @@ class EditPage { } } - if(!$this->mTitle->getArticleID() && ('initial' == $this->formtype || $this->firsttime )) { # new article + # Show applicable editing introductions + if( $this->formtype == 'initial' || $this->firsttime ) $this->showIntro(); - } + if( $this->mTitle->isTalkPage() ) { $wgOut->addWikiText( wfMsg( 'talkpagetext' ) ); } @@ -449,17 +436,30 @@ class EditPage { } /** - * Return true if this page should be previewed when the edit form - * is initially opened. + * Should we show a preview when the edit form is first shown? + * * @return bool - * @private */ - function previewOnOpen() { - global $wgUser; - return $this->section != 'new' && - ( ( $wgUser->getOption( 'previewonfirst' ) && $this->mTitle->exists() ) || - ( $this->mTitle->getNamespace() == NS_CATEGORY && - !$this->mTitle->exists() ) ); + private function previewOnOpen() { + global $wgRequest, $wgUser; + if( $wgRequest->getVal( 'preview' ) == 'yes' ) { + // Explicit override from request + return true; + } elseif( $wgRequest->getVal( 'preview' ) == 'no' ) { + // Explicit override from request + return false; + } elseif( $this->section == 'new' ) { + // Nothing *to* preview for new sections + return false; + } elseif( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { + // Standard preference behaviour + return true; + } elseif( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { + // Categories are special + return true; + } else { + return false; + } } /** @@ -547,9 +547,10 @@ class EditPage { $this->summary = ''; $this->edittime = ''; $this->starttime = wfTimestampNow(); + $this->edit = false; $this->preview = false; $this->save = false; - $this->diff = false; + $this->diff = false; $this->minoredit = false; $this->watchthis = false; $this->recreate = false; @@ -575,35 +576,45 @@ class EditPage { */ function tokenOk( &$request ) { global $wgUser; - if( $wgUser->isAnon() ) { - # Anonymous users may not have a session - # open. Check for suffix anyway. - $this->mTokenOk = ( EDIT_TOKEN_SUFFIX == $request->getVal( 'wpEditToken' ) ); - } else { - $this->mTokenOk = $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); - } + $token = $request->getVal( 'wpEditToken' ); + $this->mTokenOk = $wgUser->matchEditToken( $token ); + $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token ); return $this->mTokenOk; } - /** */ - function showIntro() { + /** + * Show all applicable editing introductions + */ + private function showIntro() { global $wgOut, $wgUser; - $addstandardintro=true; - if($this->editintro) { - $introtitle=Title::newFromText($this->editintro); - if(isset($introtitle) && $introtitle->userCanRead()) { - $rev=Revision::newFromTitle($introtitle); - if($rev) { - $wgOut->addSecondaryWikiText($rev->getText()); - $addstandardintro=false; - } - } - } - if($addstandardintro) { - if ( $wgUser->isLoggedIn() ) + if( !$this->showCustomIntro() && !$this->mTitle->exists() ) { + if( $wgUser->isLoggedIn() ) { $wgOut->addWikiText( wfMsg( 'newarticletext' ) ); - else + } else { $wgOut->addWikiText( wfMsg( 'newarticletextanon' ) ); + } + $this->showDeletionLog( $wgOut ); + } + } + + /** + * Attempt to show a custom editing introduction, if supplied + * + * @return bool + */ + private function showCustomIntro() { + if( $this->editintro ) { + $title = Title::newFromText( $this->editintro ); + if( $title instanceof Title && $title->exists() && $title->userCanRead() ) { + global $wgOut; + $revision = Revision::newFromTitle( $title ); + $wgOut->addSecondaryWikiText( $revision->getText() ); + return true; + } else { + return false; + } + } else { + return false; } } @@ -762,7 +773,7 @@ class EditPage { if ( $this->isConflict) { wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . - $this->mArticle->getTimestamp() . "'\n" ); + $this->mArticle->getTimestamp() . "')\n" ); $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime); } else { @@ -777,7 +788,7 @@ class EditPage { # Suppress edit conflict with self, except for section edits where merging is required. if ( ( $this->section == '' ) && ( 0 != $userid ) && ( $this->mArticle->getUser() == $userid ) ) { - wfDebug( "Suppressing edit conflict, same user.\n" ); + wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); $this->isConflict = false; } else { # switch from section editing to normal editing in edit conflict @@ -786,11 +797,11 @@ class EditPage { if( $this->mergeChangesInto( $text ) ){ // Successful merge! Maybe we should tell the user the good news? $this->isConflict = false; - wfDebug( "Suppressing edit conflict, successful merge.\n" ); + wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); } else { $this->section = ''; $this->textbox1 = $text; - wfDebug( "Keeping edit conflict, failed merge.\n" ); + wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); } } } @@ -831,6 +842,10 @@ class EditPage { } if( $this->summary != '' ) { $sectionanchor = $this->sectionAnchor( $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}]]"; } } elseif( $this->section != '' ) { # Try to get a section anchor from the section source, redirect to edited section if header found @@ -909,6 +924,10 @@ class EditPage { # Enabled article-related sidebar, toplinks, etc. $wgOut->setArticleRelated( true ); + if ( $this->formtype == 'preview' ) { + $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); + } + if ( $this->isConflict ) { $s = wfMsg( 'editconflict', $this->mTitle->getPrefixedText() ); $wgOut->setPageTitle( $s ); @@ -1002,13 +1021,14 @@ class EditPage { } if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? - list($cascadeSources, $restrictions) = $this->mTitle->getCascadeProtectionSources(); + list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); if ( count($cascadeSources) > 0 ) { # Explain, and list the titles responsible $notice = wfMsgExt( 'cascadeprotectedwarning', array('parsemag'), count($cascadeSources) ) . "\n"; - foreach( $cascadeSources as $id => $page ) + foreach( $cascadeSources as $page ) { $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; } + } $wgOut->addWikiText( $notice ); } @@ -1089,7 +1109,7 @@ class EditPage { } if ( 'diff' == $this->formtype ) { - $wgOut->addHTML( $this->getDiff() ); + $this->showDiff(); } } @@ -1115,7 +1135,7 @@ class EditPage { if( !$this->preview && !$this->diff ) { $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); } - $templates = ($this->preview || $this->section) ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates(); + $templates = ($this->preview || $this->section != '') ? $this->mPreviewTemplates : $this->mArticle->getUsedTemplates(); $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); global $wgUseMetadataEdit ; @@ -1229,10 +1249,7 @@ END * include the constant suffix to prevent editing from * broken text-mangling proxies. */ - if ( $wgUser->isLoggedIn() ) - $token = htmlspecialchars( $wgUser->editToken() ); - else - $token = EDIT_TOKEN_SUFFIX; + $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( "\n\n" ); @@ -1271,7 +1288,7 @@ END } if ( $this->formtype == 'diff') { - $wgOut->addHTML( $this->getDiff() ); + $this->showDiff(); } } @@ -1292,6 +1309,7 @@ END if($this->mTitle->getNamespace() == NS_CATEGORY) { $this->mArticle->openShowCategory(); } + wfRunHooks( 'OutputPageBeforeHTML',array( &$wgOut, &$text ) ); $wgOut->addHTML( $text ); if($this->mTitle->getNamespace() == NS_CATEGORY) { $this->mArticle->closeShowCategory(); @@ -1363,7 +1381,11 @@ END wfProfileIn( $fname ); if ( $this->mTriedSave && !$this->mTokenOk ) { - $msg = 'session_fail_preview'; + if ( $this->mTokenOkExceptSuffix ) { + $msg = 'token_suffix_mismatch'; + } else { + $msg = 'session_fail_preview'; + } } else { $msg = 'previewnote'; } @@ -1414,6 +1436,9 @@ END $previewHTML = $parserOutput->getText(); $wgOut->addParserOutputNoText( $parserOutput ); + + # ParserOutput might have altered the page title, so reset it + $wgOut->setPageTitle( wfMsg( 'editing', $this->mTitle->getPrefixedText() ) ); foreach ( $parserOutput->getTemplates() as $ns => $template) foreach ( array_keys( $template ) as $dbk) @@ -1497,7 +1522,7 @@ END $wgOut->setArticleRelated( false ); $wgOut->addWikiText( wfMsg( 'nosuchsectiontext', $this->section ) ); - $wgOut->returnToMain( false ); + $wgOut->returnToMain( false, $this->mTitle->getPrefixedUrl() ); } /** @@ -1893,10 +1918,8 @@ END * * If this is a section edit, we'll replace the section as for final * save and then make a comparison. - * - * @return string HTML */ - function getDiff() { + function showDiff() { $oldtext = $this->mArticle->fetchContent(); $newtext = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); @@ -1907,11 +1930,13 @@ END $de = new DifferenceEngine( $this->mTitle ); $de->setText( $oldtext, $newtext ); $difftext = $de->getDiff( $oldtitle, $newtitle ); + $de->showDiffStyle(); } else { $difftext = ''; } - return '
' . $difftext . '
'; + global $wgOut; + $wgOut->addHtml( '
' . $difftext . '
' ); } /** @@ -2034,7 +2059,32 @@ END $wgOut->setPageTitle( wfMsg( 'nocreatetitle' ) ); $wgOut->addWikiText( wfMsg( 'nocreatetext' ) ); } - + + /** + * If there are rows in the deletion log for this page, show them, + * along with a nice little note for the user + * + * @param OutputPage $out + */ + private function showDeletionLog( $out ) { + $title = $this->mArticle->getTitle(); + $reader = new LogReader( + new FauxRequest( + array( + 'page' => $title->getPrefixedText(), + 'type' => 'delete', + ) + ) + ); + if( $reader->hasRows() ) { + $out->addHtml( '
' ); + $out->addWikiText( wfMsg( 'recreate-deleted-warn' ) ); + $viewer = new LogViewer( $reader ); + $viewer->showList( $out ); + $out->addHtml( '
' ); + } + } + } -?> + diff --git a/includes/EmaillingJob.php b/includes/EmaillingJob.php new file mode 100644 index 00000000..73d71c56 --- /dev/null +++ b/includes/EmaillingJob.php @@ -0,0 +1,25 @@ +params['to'], + $this->params['from'], + $this->params['subj'], + $this->params['body'], + $this->params['replyto'] + ); + return true; + } + +} + diff --git a/includes/EnotifNotifyJob.php b/includes/EnotifNotifyJob.php new file mode 100644 index 00000000..70d1de69 --- /dev/null +++ b/includes/EnotifNotifyJob.php @@ -0,0 +1,26 @@ +actuallyNotifyOnPageChange( + User::newFromName( $this->params['editor'], false ), + $this->title, + $this->params['timestamp'], + $this->params['summary'], + $this->params['minorEdit'], + $this->params['oldid'] + ); + return true; + } + +} + diff --git a/includes/Exception.php b/includes/Exception.php index 4cf0b7ba..02819cc9 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -235,4 +235,4 @@ function wfExceptionHandler( $e ) { exit( 1 ); } -?> + diff --git a/includes/Exif.php b/includes/Exif.php index 3a06ca1b..d98a8e0d 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -405,7 +405,7 @@ class Exif { * * @return int */ - function version() { + public static function version() { return 1; // We don't need no bloddy constants! } @@ -1131,4 +1131,4 @@ define( 'MW_EXIF_UNDEFINED', Exif::UNDEFINED ); define( 'MW_EXIF_SLONG', Exif::SLONG ); define( 'MW_EXIF_SRATIONAL', Exif::SRATIONAL ); -?> + diff --git a/includes/Export.php b/includes/Export.php index 9307795d..c3ef9451 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -558,7 +558,7 @@ class Dump7ZipOutput extends DumpPipeOutput { $command = "7za a -bd -si " . wfEscapeShellArg( $file ); // Suppress annoying useless crap from p7zip // Unfortunately this could suppress real error messages too - $command .= " >/dev/null 2>&1"; + $command .= ' >' . wfGetNull() . ' 2>&1'; parent::DumpPipeOutput( $command ); } } @@ -767,4 +767,4 @@ function xmlsafe( $string ) { return $string; } -?> + diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index c8ed8bde..f3fc22e3 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -46,7 +46,7 @@ class ExternalEdit { $extension="wiki"; } elseif($this->mMode=="file") { $type="Edit file"; - $image = new Image( $this->mTitle ); + $image = wfLocalFile( $this->mTitle ); $img_url = $image->getURL(); if(strpos($img_url,"://")) { $url = $img_url; @@ -72,4 +72,4 @@ CONTROL; echo $control; } } -?> + diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index fb66b652..5efc6e25 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -41,10 +41,9 @@ class ExternalStore { return false; $class='ExternalStore'.ucfirst($proto); - /* Preloaded modules might exist, especially ones serving multiple protocols */ + /* Any custom modules should be added to $wgAutoLoadClasses for on-demand loading */ if (!class_exists($class)) { - if (!include_once($class.'.php')) - return false; + return false; } $store=new $class(); return $store; @@ -66,4 +65,4 @@ class ExternalStore { } } } -?> + diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 7b4ffc2f..f9046f74 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -144,4 +144,4 @@ class ExternalStoreDB { return "DB://$cluster/$id"; } } -?> + diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php index e6656986..cff6c4d4 100644 --- a/includes/ExternalStoreHttp.php +++ b/includes/ExternalStoreHttp.php @@ -19,4 +19,4 @@ class ExternalStoreHttp { * whatever, for initial ext storage */ } -?> + diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index 293bdaf0..b63ae505 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -84,4 +84,4 @@ class FakeTitle { function trackbackRDF() { $this->error(); } } -?> + diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php new file mode 100644 index 00000000..ee165cd1 --- /dev/null +++ b/includes/FileDeleteForm.php @@ -0,0 +1,220 @@ + + */ +class FileDeleteForm { + + private $title = null; + private $file = null; + + private $oldfile = null; + private $oldimage = ''; + + /** + * Constructor + * + * @param File $file File we're deleting + */ + public function __construct( $file ) { + $this->title = $file->getTitle(); + $this->file = $file; + } + + /** + * Fulfil the request; shows the form or deletes the file, + * pending authentication, confirmation, etc. + */ + public function execute() { + global $wgOut, $wgRequest, $wgUser; + $this->setHeaders(); + + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } elseif( !$wgUser->isLoggedIn() ) { + $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); + return; + } elseif( !$wgUser->isAllowed( 'delete' ) ) { + $wgOut->permissionError( 'delete' ); + return; + } elseif( $wgUser->isBlocked() ) { + $wgOut->blockedPage(); + return; + } + + $this->oldimage = $wgRequest->getText( 'oldimage', false ); + $token = $wgRequest->getText( 'wpEditToken' ); + if( $this->oldimage && !$this->isValidOldSpec() ) { + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) ); + return; + } + if( $this->oldimage ) + $this->oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); + + if( !$this->haveDeletableFile() ) { + $wgOut->addHtml( $this->prepareMessage( 'filedelete-nofile' ) ); + $wgOut->addReturnTo( $this->title ); + return; + } + + // Perform the deletion if appropriate + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { + $comment = $wgRequest->getText( 'wpReason' ); + if( $this->oldimage ) { + $status = $this->file->deleteOld( $this->oldimage, $comment ); + if( $status->ok ) { + // Need to do a log item + $log = new LogPage( 'delete' ); + $log->addEntry( 'delete', $this->title, wfMsg( 'deletedrevision' , $this->oldimage ) ); + } + } else { + $status = $this->file->delete( $comment ); + if( $status->ok ) { + // Need to delete the associated article + $article = new Article( $this->title ); + $article->doDeleteArticle( $comment ); + } + } + if( !$status->isGood() ) + $wgOut->addWikiText( $status->getWikiText( 'filedeleteerror-short', 'filedeleteerror-long' ) ); + if( $status->ok ) { + $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 + $wgOut->addReturnTo( $this->oldimage ? $this->title : Title::newMainPage() ); + } + return; + } + + $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 .= '
' . wfMsgHtml( 'filedelete-legend' ) . ''; + $form .= $this->prepareMessage( 'filedelete-intro' ); + + $form .= '

' . Xml::inputLabel( wfMsg( 'filedelete-comment' ), 'wpReason', 'wpReason', + 60, $wgRequest->getText( 'wpReason' ) ) . '

'; + $form .= '

' . Xml::submitButton( wfMsg( 'filedelete-submit' ) ) . '

'; + $form .= '
'; + $form .= ''; + + $wgOut->addHtml( $form ); + } + + /** + * Show deletion log fragments pertaining to the current file + */ + private function showLogEntries() { + global $wgOut; + $wgOut->addHtml( '

' . htmlspecialchars( LogPage::logName( 'delete' ) ) . "

\n" ); + $reader = new LogViewer( + new LogReader( + new FauxRequest( + array( + 'type' => 'delete', + 'page' => $this->title->getPrefixedText(), + ) + ) + ) + ); + $reader->showList( $wgOut ); + } + + /** + * Prepare a message referring to the file being deleted, + * showing an appropriate message depending upon whether + * it's a current file or an old version + * + * @param string $message Message base + * @return string + */ + private function prepareMessage( $message ) { + global $wgLang, $wgServer; + if( $this->oldimage ) { + return wfMsgExt( + "{$message}-old", + 'parse', + $this->title->getText(), + $wgLang->date( $this->getTimestamp(), true ), + $wgLang->time( $this->getTimestamp(), true ), + $wgServer . $this->file->getArchiveUrl( $this->oldimage ) + ); + } else { + return wfMsgExt( + $message, + 'parse', + $this->title->getText() + ); + } + } + + /** + * Set headers, titles and other bits + */ + private function setHeaders() { + global $wgOut, $wgUser; + $wgOut->setPageTitle( wfMsg( 'filedelete', $this->title->getText() ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setSubtitle( wfMsg( 'filedelete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) ); + } + + /** + * Is the provided `oldimage` value valid? + * + * @return bool + */ + private function isValidOldSpec() { + return strlen( $this->oldimage ) >= 16 + && strpos( $this->oldimage, '/' ) === false + && strpos( $this->oldimage, '\\' ) === false; + } + + /** + * Could we delete the file specified? If an `oldimage` + * value was provided, does it correspond to an + * existing, local, old version of this file? + * + * @return bool + */ + private function haveDeletableFile() { + return $this->oldimage + ? $this->oldfile && $this->oldfile->exists() && $this->oldfile->isLocal() + : $this->file && $this->file->exists() && $this->file->isLocal(); + } + + /** + * Prepare the form action + * + * @return string + */ + private function getAction() { + $q = array(); + $q[] = 'action=delete'; + if( $this->oldimage ) + $q[] = 'oldimage=' . urlencode( $this->oldimage ); + return $this->title->getLocalUrl( implode( '&', $q ) ); + } + + /** + * Extract the timestamp of the old version + * + * @return string + */ + private function getTimestamp() { + return $this->oldfile->getTimestamp(); + } + +} \ No newline at end of file diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php new file mode 100644 index 00000000..55f21fff --- /dev/null +++ b/includes/FileRevertForm.php @@ -0,0 +1,165 @@ + + */ +class FileRevertForm { + + private $title = null; + private $file = null; + private $oldimage = ''; + private $timestamp = false; + + /** + * Constructor + * + * @param File $file File we're reverting + */ + public function __construct( $file ) { + $this->title = $file->getTitle(); + $this->file = $file; + } + + /** + * Fulfil the request; shows the form or reverts the file, + * pending authentication, confirmation, etc. + */ + public function execute() { + global $wgOut, $wgRequest, $wgUser, $wgLang, $wgServer; + $this->setHeaders(); + + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return; + } elseif( !$wgUser->isLoggedIn() ) { + $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); + return; + } elseif( !$this->title->userCan( 'edit' ) ) { + // The standard read-only thing doesn't make a whole lot of sense + // here; surely it should show the image or something? -- RC + $article = new Article( $this->title ); + $wgOut->readOnlyPage( $article->getContent(), true ); + return; + } elseif( $wgUser->isBlocked() ) { + $wgOut->blockedPage(); + return; + } + + $this->oldimage = $wgRequest->getText( 'oldimage' ); + $token = $wgRequest->getText( 'wpEditToken' ); + if( !$this->isValidOldSpec() ) { + $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) ); + return; + } + + if( !$this->haveOldVersion() ) { + $wgOut->addHtml( wfMsgExt( 'filerevert-badversion', 'parse' ) ); + $wgOut->returnToMain( false, $this->title ); + return; + } + + // Perform the reversion if appropriate + if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) { + $source = $this->file->getArchiveVirtualUrl( $this->oldimage ); + $comment = $wgRequest->getText( 'wpComment' ); + // TODO: Preserve file properties from database instead of reloading from file + $status = $this->file->upload( $source, $comment, $comment ); + if( $status->isGood() ) { + $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 ) ) ); + $wgOut->returnToMain( false, $this->title ); + } else { + $wgOut->addWikiText( $status->getWikiText() ); + } + return; + } + + // Show the form + $this->showForm(); + } + + /** + * Show the confirmation form + */ + private function showForm() { + global $wgOut, $wgUser, $wgRequest, $wgLang, $wgContLang, $wgServer; + $timestamp = $this->getTimestamp(); + + $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ); + $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken( $this->oldimage ) ); + $form .= '
' . wfMsgHtml( 'filerevert-legend' ) . ''; + $form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(), + $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), $wgServer . $this->file->getArchiveUrl( $this->oldimage ) ); + $form .= '

' . Xml::inputLabel( wfMsg( 'filerevert-comment' ), 'wpComment', 'wpComment', + 60, wfMsgForContent( 'filerevert-defaultcomment', + $wgContLang->date( $timestamp, false, false ), $wgContLang->time( $timestamp, false, false ) ) ) . '

'; + $form .= '

' . Xml::submitButton( wfMsg( 'filerevert-submit' ) ) . '

'; + $form .= '
'; + $form .= ''; + + $wgOut->addHtml( $form ); + } + + /** + * Set headers, titles and other bits + */ + private function setHeaders() { + global $wgOut, $wgUser; + $wgOut->setPageTitle( wfMsg( 'filerevert', $this->title->getText() ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); + $wgOut->setSubtitle( wfMsg( 'filerevert-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) ); + } + + /** + * Is the provided `oldimage` value valid? + * + * @return bool + */ + private function isValidOldSpec() { + return strlen( $this->oldimage ) >= 16 + && strpos( $this->oldimage, '/' ) === false + && strpos( $this->oldimage, '\\' ) === false; + } + + /** + * Does the provided `oldimage` value correspond + * to an existing, local, old version of this file? + * + * @return bool + */ + private function haveOldVersion() { + $file = wfFindFile( $this->title, $this->oldimage ); + return $file && $file->exists() && $file->isLocal(); + } + + /** + * Prepare the form action + * + * @return string + */ + private function getAction() { + $q = array(); + $q[] = 'action=revert'; + $q[] = 'oldimage=' . urlencode( $this->oldimage ); + return $this->title->getLocalUrl( implode( '&', $q ) ); + } + + /** + * Extract the timestamp of the old version + * + * @return string + */ + private function getTimestamp() { + if( $this->timestamp === false ) { + $file = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->oldimage ); + $this->timestamp = $file->getTimestamp(); + } + return $this->timestamp; + } + +} \ No newline at end of file diff --git a/includes/FileStore.php b/includes/FileStore.php index dcec71c5..1554d66e 100644 --- a/includes/FileStore.php +++ b/includes/FileStore.php @@ -219,7 +219,7 @@ class FileStore { * Confirm that the given file key is valid. * Note that a valid key may refer to a file that does not exist. * - * Key should consist of a 32-digit base-36 SHA-1 hash and + * Key should consist of a 31-digit base-36 SHA-1 hash and * an optional alphanumeric extension, all lowercase. * The whole must not exceed 64 characters. * @@ -227,7 +227,7 @@ class FileStore { * @return boolean */ static function validKey( $key ) { - return preg_match( '/^[0-9a-z]{32}(\.[0-9a-z]{1,31})?$/', $key ); + return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key ); } @@ -249,7 +249,7 @@ class FileStore { return false; } - $base36 = wfBaseConvert( $hash, 16, 36, 32 ); + $base36 = wfBaseConvert( $hash, 16, 36, 31 ); if( $extension == '' ) { $key = $base36; } else { @@ -376,4 +376,4 @@ class FSTransaction { */ class FSException extends MWException { } -?> + diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 1ffde741..67cc1f39 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -429,18 +429,11 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = * @param $key String: */ function wfMsgWeirdKey ( $key ) { - $subsource = str_replace ( ' ' , '_' , $key ) ; - $source = wfMsgForContentNoTrans( $subsource ) ; - if ( wfEmptyMsg( $subsource, $source) ) { - # Try again with first char lower case - $subsource = strtolower ( substr ( $subsource , 0 , 1 ) ) . substr ( $subsource , 1 ) ; - $source = wfMsgForContentNoTrans( $subsource ) ; - } - if ( wfEmptyMsg( $subsource, $source ) ) { - # Didn't work either, return blank text - $source = "" ; - } - return $source ; + $source = wfMsgGetKey( $key, false, true, false ); + if ( wfEmptyMsg( $key, $source ) ) + return ""; + else + return $source; } /** @@ -454,6 +447,17 @@ function wfMsgWeirdKey ( $key ) { function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { global $wgParser, $wgContLang, $wgMessageCache, $wgLang; + /* btw, is all that code in wfMsgGetKey() that check + * if the message cache exists of not really necessary, or is + * it just paranoia? + * Vyznev: it's probably not necessary + * I think I wrote it in an attempt to report DB + * connection errors properly + * but eventually we gave up on using the + * message cache for that and just hard-coded the strings + * it may have other uses, it's not mere paranoia + */ + if ( is_object( $wgMessageCache ) ) $transstat = $wgMessageCache->getTransform(); @@ -468,16 +472,18 @@ function wfMsgGetKey( $key, $useDB, $forContent = false, $transform = true ) { $lang = &$wgLang; } - wfSuppressWarnings(); + # MessageCache::get() does this already, Language::getMessage() doesn't + # 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($message === false) - $message = Language::getMessage($key); + if ( $transform && strstr( $message, '{{' ) !== false ) { $message = $wgParser->transformMsg($message, $wgMessageCache->getParserOptions() ); } @@ -586,7 +592,7 @@ function wfMsgExt( $key, $options ) { } elseif ( in_array('parseinline', $options) ) { $string = $wgOut->parse( $string, true, true ); $m = array(); - if( preg_match( "~^

(.*)\n?

$~", $string, $m ) ) { + if( preg_match( '/^

(.*)\n?<\/p>$/sU', $string, $m ) ) { $string = $m[1]; } } elseif ( in_array('parsemag', $options) ) { @@ -695,14 +701,14 @@ function wfHostname() { * @return string */ function wfReportTime() { - global $wgRequestTime; + global $wgRequestTime, $wgShowHostnames; $now = wfTime(); $elapsed = $now - $wgRequestTime; - $com = sprintf( "", - wfHostname(), $elapsed ); - return $com; + return $wgShowHostnames + ? sprintf( "", wfHostname(), $elapsed ) + : sprintf( "", $elapsed ); } /** @@ -813,7 +819,7 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { if ( $po < 0 ) { $po = 0; } $q = "limit={$limit}&offset={$po}"; if ( '' != $query ) { $q .= '&'.$query; } - $plink = '{$prev}"; + $plink = '{$prev}"; } else { $plink = $prev; } $no = $offset + $limit; @@ -823,7 +829,7 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { if ( $atend ) { $nlink = $next; } else { - $nlink = '{$next}"; + $nlink = '{$next}"; } $nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' . wfNumLink( $offset, 50, $title, $query ) . ' | ' . @@ -844,7 +850,7 @@ function wfNumLink( $offset, $limit, &$title, $query = '' ) { $q .= 'limit='.$limit.'&offset='.$offset; $fmtLimit = $wgLang->formatNum( $limit ); - $s = '{$fmtLimit}"; + $s = '{$fmtLimit}"; return $s; } @@ -1657,47 +1663,11 @@ function wfTempDir() { * Make directory, and make all parent directories if they don't exist */ function wfMkdirParents( $fullDir, $mode = 0777 ) { - if ( strval( $fullDir ) === '' ) { + if( strval( $fullDir ) === '' ) return true; - } - - # Go back through the paths to find the first directory that exists - $currentDir = $fullDir; - $createList = array(); - while ( strval( $currentDir ) !== '' && !file_exists( $currentDir ) ) { - # Strip trailing slashes - $currentDir = rtrim( $currentDir, '/\\' ); - - # Add to create list - $createList[] = $currentDir; - - # Find next delimiter searching from the end - $p = max( strrpos( $currentDir, '/' ), strrpos( $currentDir, '\\' ) ); - if ( $p === false ) { - $currentDir = false; - } else { - $currentDir = substr( $currentDir, 0, $p ); - } - } - - if ( count( $createList ) == 0 ) { - # Directory specified already exists + if( file_exists( $fullDir ) ) return true; - } elseif ( $currentDir === false ) { - # Went all the way back to root and it apparently doesn't exist - return false; - } - - # Now go forward creating directories - $createList = array_reverse( $createList ); - foreach ( $createList as $dir ) { - # use chmod to override the umask, as suggested by the PHP manual - if ( !mkdir( $dir, $mode ) || !chmod( $dir, $mode ) ) { - wfDebugLog( 'mkdir', "Unable to create directory $dir\n" ); - return false; - } - } - return true; + return mkdir( str_replace( '/', DIRECTORY_SEPARATOR, $fullDir ), $mode, true ); } /** @@ -1761,7 +1731,7 @@ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { * @return bool */ function wfEmptyMsg( $msg, $wfMsgOut ) { - return $wfMsgOut === "<$msg>"; + return $wfMsgOut === htmlspecialchars( "<$msg>" ); } /** @@ -1902,11 +1872,15 @@ function wfRegexReplacement( $string ) { * We'll consider it so always, as we don't want \s in our Unix paths either. * * @param string $path + * @param string $suffix to remove if present * @return string */ -function wfBaseName( $path ) { +function wfBaseName( $path, $suffix='' ) { + $encSuffix = ($suffix == '') + ? '' + : ( '(?:' . preg_quote( $suffix, '#' ) . ')?' ); $matches = array(); - if( preg_match( '#([^/\\\\]*)[/\\\\]*$#', $path, $matches ) ) { + if( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { return $matches[1]; } else { return ''; @@ -2266,4 +2240,84 @@ function &wfGetDB( $db = DB_LAST, $groups = array() ) { $ret = $wgLoadBalancer->getConnection( $db, true, $groups ); return $ret; } -?> + +/** + * Find a file. + * Shortcut for RepoGroup::singleton()->findFile() + * @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. + * @return File, or false if the file does not exist + */ +function wfFindFile( $title, $time = false ) { + return RepoGroup::singleton()->findFile( $title, $time ); +} + +/** + * Get an object referring to a locally registered file. + * Returns a valid placeholder object if the file does not exist. + */ +function wfLocalFile( $title ) { + return RepoGroup::singleton()->getLocalRepo()->newFile( $title ); +} + +/** + * Should low-performance queries be disabled? + * + * @return bool + */ +function wfQueriesMustScale() { + global $wgMiserMode; + return $wgMiserMode + || ( SiteStats::pages() > 100000 + && SiteStats::edits() > 1000000 + && SiteStats::users() > 10000 ); +} + +/** + * Get the path to a specified script file, respecting file + * extensions; this is a wrapper around $wgScriptExtension etc. + * + * @param string $script Script filename, sans extension + * @return string + */ +function wfScript( $script = 'index' ) { + global $wgScriptPath, $wgScriptExtension; + return "{$wgScriptPath}/{$script}{$wgScriptExtension}"; +} + +/** + * Convenience function converts boolean values into "true" + * or "false" (string) values + * + * @param bool $value + * @return string + */ +function wfBoolToStr( $value ) { + return $value ? 'true' : 'false'; +} + +/** + * Load an extension messages file + */ +function wfLoadExtensionMessages( $extensionName ) { + global $wgExtensionMessagesFiles, $wgMessageCache; + if ( !empty( $wgExtensionMessagesFiles[$extensionName] ) ) { + $wgMessageCache->loadMessagesFile( $wgExtensionMessagesFiles[$extensionName] ); + // Prevent double-loading + $wgExtensionMessagesFiles[$extensionName] = false; + } +} + +/** + * Get a platform-independent path to the null file, e.g. + * /dev/null + * + * @return string + */ +function wfGetNull() { + return wfIsWindows() + ? 'NUL' + : '/dev/null'; +} \ No newline at end of file diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 9a0b6a08..260439b2 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -67,13 +67,13 @@ class HTMLCacheUpdate break; } } - if ( $id !== false ) { - // One less on the end to avoid duplicating the boundary - $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, $id - 1 ); - } else { - $job = new HTMLCacheUpdateJob( $this->mTitle, $this->mTable, $start, false ); - } - $jobs[] = $job; + + $params = array( + 'table' => $this->mTable, + 'start' => $start, + 'end' => ( $id !== false ? $id - 1 : false ), + ); + $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); $start = $id; } while ( $start ); @@ -193,20 +193,14 @@ class HTMLCacheUpdateJob extends Job { /** * Construct a job * @param Title $title The title linked to - * @param string $table The name of the link table. - * @param integer $start Beginning page_id or false for open interval - * @param integer $end End page_id or false for open interval + * @param array $params Job parameters (table, start and end page_ids) * @param integer $id job_id */ - function __construct( $title, $table, $start, $end, $id = 0 ) { - $params = array( - 'table' => $table, - 'start' => $start, - 'end' => $end ); + function __construct( $title, $params, $id = 0 ) { parent::__construct( 'htmlCacheUpdate', $title, $params, $id ); - $this->table = $table; - $this->start = intval( $start ); - $this->end = intval( $end ); + $this->table = $params['table']; + $this->start = $params['start']; + $this->end = $params['end']; } function run() { @@ -229,4 +223,4 @@ class HTMLCacheUpdateJob extends Job { return true; } } -?> + diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php index 1d3778b2..a7466814 100644 --- a/includes/HTMLFileCache.php +++ b/includes/HTMLFileCache.php @@ -154,4 +154,4 @@ class HTMLFileCache { } -?> + diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index 715c8c88..69ec1007 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -1,7 +1,6 @@ {$s}\n"; } } // end class - -/** Build a select with all defined groups - * - * used by SpecialUserrights.php - * @todo move it to there, and don't forget to copy it for SpecialMakesysop.php - * - * @param $selectname String: name of this element. Name of form is automaticly prefixed. - * @param $selectmsg String: FIXME - * @param $selected Array: array of element selected when posted. Only multiples will show them. - * @param $multiple Boolean: A multiple elements select. - * @param $size Integer: number of elements to be shown ignored for non-multiple (default 6). - * @param $reverse Boolean: if true, multiple select will hide selected elements (default false). - * @todo Document $selectmsg -*/ -function HTMLSelectGroups($selectname, $selectmsg, $selected=array(), $multiple=false, $size=6, $reverse=false) { - $groups = User::getAllGroups(); - $out = htmlspecialchars( wfMsg( $selectmsg ) ); - $out .= "
"; - - if( $multiple ) { - $attribs = array( - 'name' => $selectname . '[]', - 'multiple'=> 'multiple', - 'size' => $size ); - } else { - $attribs = array( 'name' => $selectname ); - } - $attribs['style'] = 'width: 100%'; - $out .= wfElement( 'select', $attribs, null ); - - foreach( $groups as $group ) { - $attribs = array( 'value' => $group ); - if( $multiple ) { - // for multiple will only show the things we want - if( !in_array( $group, $selected ) xor $reverse ) { - continue; - } - } else { - if( in_array( $group, $selected ) ) { - $attribs['selected'] = 'selected'; - } - } - $out .= wfElement( 'option', $attribs, User::getGroupName( $group ) ) . "\n"; - } - - $out .= "\n"; - return $out; -} - -?> diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 9dfd6d61..984ee2d4 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -310,4 +310,4 @@ class HistoryBlobCurStub { } -?> + diff --git a/includes/Hooks.php b/includes/Hooks.php index b428b08d..20103db4 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -119,6 +119,20 @@ function wfRunHooks($event, $args = null) { global $wgOut; $wgOut->showFatalError($retval); return false; + } elseif( $retval === null ) { + if( is_array( $callback ) ) { + if( is_object( $callback[0] ) ) { + $prettyClass = get_class( $callback[0] ); + } else { + $prettyClass = strval( $callback[0] ); + } + $prettyFunc = $prettyClass . '::' . strval( $callback[1] ); + } else { + $prettyFunc = strval( $callback ); + } + throw new MWException( "Detected bug in an extension! " . + "Hook $prettyFunc failed to return a value; " . + "should return true to continue hook processing or false to abort." ); } else if (!$retval) { return false; } @@ -126,4 +140,4 @@ function wfRunHooks($event, $args = null) { return true; } -?> + diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index a9fb13ca..6ea3abd0 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -4,14 +4,23 @@ * Various HTTP related functions */ class Http { + static function get( $url, $timeout = 'default' ) { + return Http::request( "GET", $url, $timeout ); + } + + static function post( $url, $timeout = 'default' ) { + return Http::request( "POST", $url, $timeout ); + } + /** * Get the contents of a file by HTTP * * if $timeout is 'default', $wgHTTPTimeout is used */ - static function get( $url, $timeout = 'default' ) { + static function request( $method, $url, $timeout = 'default' ) { global $wgHTTPTimeout, $wgHTTPProxy, $wgVersion, $wgTitle; + wfDebug( __METHOD__ . ": $method $url\n" ); # Use curl if available if ( function_exists( 'curl_init' ) ) { $c = curl_init( $url ); @@ -26,6 +35,10 @@ class Http { } curl_setopt( $c, CURLOPT_TIMEOUT, $timeout ); curl_setopt( $c, CURLOPT_USERAGENT, "MediaWiki/$wgVersion" ); + if ( $method == 'POST' ) + curl_setopt( $c, CURLOPT_POST, true ); + else + curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $method ); # Set the referer to $wgTitle, even in command-line mode # This is useful for interwiki transclusion, where the foreign @@ -45,12 +58,29 @@ class Http { if ( curl_getinfo( $c, CURLINFO_HTTP_CODE ) != 200 ) { $text = false; } + # Don't return truncated output + if ( curl_errno( $c ) != CURLE_OK ) { + $text = false; + } curl_close( $c ); } else { - # Otherwise use file_get_contents, or its compatibility function from GlobalFunctions.php + # Otherwise use file_get_contents... # This may take 3 minutes to time out, and doesn't have local fetch capabilities + + global $wgVersion; + $headers = array( "User-Agent: MediaWiki/$wgVersion" ); + if( strcasecmp( $method, 'post' ) == 0 ) { + // Required for HTTP 1.0 POSTs + $headers[] = "Content-Length: 0"; + } + $opts = array( + 'http' => array( + 'method' => $method, + 'header' => implode( "\r\n", $headers ) ) ); + $ctx = stream_context_create($opts); + $url_fopen = ini_set( 'allow_url_fopen', 1 ); - $text = file_get_contents( $url ); + $text = file_get_contents( $url, false, $ctx ); ini_set( 'allow_url_fopen', $url_fopen ); } return $text; @@ -88,4 +118,4 @@ class Http { return false; } } -?> + diff --git a/includes/IP.php b/includes/IP.php index 8a2756c9..db712c3b 100644 --- a/includes/IP.php +++ b/includes/IP.php @@ -22,7 +22,12 @@ define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)'); define( 'RE_IPV6_ADD', '(:(:' . RE_IPV6_WORD . '){1,7}|' . RE_IPV6_WORD . '(:{1,2}' . RE_IPV6_WORD . '|::$){1,7})' ); define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX ); // This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network -define( 'IP_ADDRESS_STRING', RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)|' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)'); +define( 'IP_ADDRESS_STRING', + '(?:' . + RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)' . + '|' . + RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)' . + ')' ); /** * A collection of public static functions to play with IP address @@ -109,13 +114,14 @@ class IP { * @return string */ public static function sanitizeIP( $ip ) { - if ( !$ip ) return null; + $ip = trim( $ip ); + if ( $ip === '' ) return null; // Trim and return IPv4 addresses - if ( self::isIPv4($ip) ) return trim($ip); + if ( self::isIPv4($ip) ) return $ip; // Only IPv6 addresses can be expanded if ( !self::isIPv6($ip) ) return $ip; // Remove any whitespaces, convert to upper case - $ip = strtoupper( trim($ip) ); + $ip = strtoupper( $ip ); // Expand zero abbreviations if ( strpos( $ip, '::' ) !== false ) { $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip); @@ -462,22 +468,29 @@ class IP { * @return valid dotted quad IPv4 address or null */ public static function canonicalize( $addr ) { - if ( self::isValid( $addr ) ) - return $addr; + if ( self::isValid( $addr ) ) + return $addr; + + // Annoying IPv6 representations like ::ffff:1.2.3.4 + if ( strpos($addr,':') !==false && strpos($addr,'.') !==false ) { + $addr = str_replace( '.', ':', $addr ); + if( IP::isIPv6( $addr ) ) + return $addr; + } - // IPv6 loopback address - $m = array(); - if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) - return '127.0.0.1'; + // IPv6 loopback address + $m = array(); + if ( preg_match( '/^0*' . RE_IPV6_GAP . '1$/', $addr, $m ) ) + return '127.0.0.1'; - // IPv4-mapped and IPv4-compatible IPv6 addresses - if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) - return $m[1]; - if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) ) - return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); + // IPv4-mapped and IPv4-compatible IPv6 addresses + if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) ) + return $m[1]; + if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) ) + return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) ); - return null; // give up + return null; // give up } } -?> + diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php index d04110d4..3e87c994 100644 --- a/includes/ImageFunctions.php +++ b/includes/ImageFunctions.php @@ -1,113 +1,4 @@ + diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index fba7714c..64f266f6 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -17,11 +17,17 @@ class ImageGallery var $mImages, $mShowBytes, $mShowFilename; var $mCaption = false; var $mSkin = false; + var $mRevisionId = 0; /** - * Is the gallery on a wiki page (i.e. not a special page) + * Hide blacklisted images? */ - var $mParsing; + var $mHideBadImages; + + /** + * Registered parser object for output callbacks + */ + var $mParser; /** * Contextual title, used when images are being screened @@ -31,6 +37,8 @@ class ImageGallery private $mPerRow = 4; // How many images wide should the gallery be? private $mWidths = 120, $mHeights = 120; // How wide/tall each thumbnail should be + + private $mAttribs = array(); /** * Create a new image gallery object. @@ -39,14 +47,22 @@ class ImageGallery $this->mImages = array(); $this->mShowBytes = true; $this->mShowFilename = true; - $this->mParsing = false; + $this->mParser = false; + $this->mHideBadImages = false; } /** - * Set the "parse" bit so we know to hide "bad" images + * Register a parser object */ - function setParsing( $val = true ) { - $this->mParsing = $val; + function setParser( $parser ) { + $this->mParser = $parser; + } + + /** + * Set bad image flag + */ + function setHideBadImages( $flag = true ) { + $this->mHideBadImages = $flag; } /** @@ -127,22 +143,30 @@ class ImageGallery /** * Add an image to the gallery. * - * @param $image Image object that is added to the gallery + * @param $title Title object of the image that is added to the gallery * @param $html String: additional HTML text to be shown. The name and size of the image are always shown. */ - function add( $image, $html='' ) { - $this->mImages[] = array( &$image, $html ); - wfDebug( "ImageGallery::add " . $image->getName() . "\n" ); + function add( $title, $html='' ) { + if ( $title instanceof File ) { + // Old calling convention + $title = $title->getTitle(); + } + $this->mImages[] = array( $title, $html ); + wfDebug( "ImageGallery::add " . $title->getText() . "\n" ); } /** * Add an image at the beginning of the gallery. * - * @param $image Image object that is added to the gallery + * @param $title Title object of the image that is added to the gallery * @param $html String: Additional HTML text to be shown. The name and size of the image are always shown. */ - function insert( $image, $html='' ) { - array_unshift( $this->mImages, array( &$image, $html ) ); + function insert( $title, $html='' ) { + if ( $title instanceof File ) { + // Old calling convention + $title = $title->getTitle(); + } + array_unshift( $this->mImages, array( &$title, $html ) ); } @@ -172,6 +196,19 @@ class ImageGallery function setShowFilename( $f ) { $this->mShowFilename = ( $f == true); } + + /** + * Set arbitrary attributes to go on the HTML gallery output element. + * Should be suitable for a <table> element. + * + * Note -- if taking from user input, you should probably run through + * Sanitizer::validateAttributes() first. + * + * @param array of HTML attribute pairs + */ + function setAttributes( $attribs ) { + $this->mAttribs = $attribs; + } /** * Return a HTML representation of the image gallery @@ -188,23 +225,33 @@ class ImageGallery $sk = $this->getSkin(); - $s = '

{$otitle}{$ntitle}{$otitle}{$ntitle}
+{$line}-{$line} {$line}$marker$line
'; + $attribs = Sanitizer::mergeAttributes( + array( + 'class' => 'gallery', + 'cellspacing' => '0', + 'cellpadding' => '0' ), + $this->mAttribs ); + $s = Xml::openElement( 'table', $attribs ); if( $this->mCaption ) $s .= "\n\t"; $params = array( 'width' => $this->mWidths, 'height' => $this->mHeights ); $i = 0; foreach ( $this->mImages as $pair ) { - $img =& $pair[0]; + $nt = $pair[0]; $text = $pair[1]; + + # Give extensions a chance to select the file revision for us + $time = false; + wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time ) ); - $nt = $img->getTitle(); + $img = wfFindFile( $nt, $time ); - if( $nt->getNamespace() != NS_IMAGE ) { + if( $nt->getNamespace() != NS_IMAGE || !$img ) { # We're dealing with a non-image, spit out the name and be done with it. $thumbhtml = "\n\t\t\t".'
' . htmlspecialchars( $nt->getText() ) . '
'; - } elseif( $this->mParsing && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) { + } elseif( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) { # The image is blacklisted, just show it as a text link. $thumbhtml = "\n\t\t\t".'
' . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '
'; @@ -214,15 +261,26 @@ class ImageGallery . htmlspecialchars( $img->getLastError() ) . ''; } else { $vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2; - $thumbhtml = "\n\t\t\t".'
' - . $sk->makeKnownLinkObj( $nt, $thumb->toHtml() ) . '
'; + + $thumbhtml = "\n\t\t\t". + '
' + # Auto-margin centering for block-level elements. Needed now that we have video + # handlers since they may emit block-level elements as opposed to simple tags. + # ref http://css-discuss.incutio.com/?page=CenteringBlockElement + . '
' + . $thumb->toHtml( array( 'desc-link' => true ) ) . '
'; + + // Call parser transform hook + if ( $this->mParser && $img->getHandler() ) { + $img->getHandler()->parserTransformHook( $this->mParser, $img ); + } } //TODO //$ul = $sk->makeLink( $wgContLang->getNsText( Namespace::getUser() ) . ":{$ut}", $ut ); if( $this->mShowBytes ) { - if( $img->exists() ) { + if( $img ) { $nb = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), $wgLang->formatNum( $img->getSize() ) ); } else { @@ -292,4 +350,5 @@ class ImageGallery } } //class -?> + + diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 13f8e46a..3cf6d0ac 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -16,8 +16,18 @@ if( !defined( 'MEDIAWIKI' ) ) class ImagePage extends Article { /* private */ var $img; // Image object this page is shown for + /* private */ var $repo; var $mExtraDescription = false; + function __construct( $title ) { + parent::__construct( $title ); + $this->img = wfFindFile( $this->mTitle ); + if ( !$this->img ) { + $this->img = wfLocalFile( $this->mTitle ); + } + $this->repo = $this->img->repo; + } + /** * Handler for action=render * Include body text only; none of the image extras @@ -31,8 +41,6 @@ class ImagePage extends Article { function view() { global $wgOut, $wgShowEXIF, $wgRequest, $wgUser; - $this->img = new Image( $this->mTitle ); - $diff = $wgRequest->getVal( 'diff' ); $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) ); @@ -40,10 +48,10 @@ class ImagePage extends Article { return Article::view(); if ($wgShowEXIF && $this->img->exists()) { - $exif = $this->img->getExifData(); - $showmeta = count($exif) ? true : false; + // FIXME: bad interface, see note on MediaHandler::formatMetadata(). + $formattedMetadata = $this->img->formatMetadata(); + $showmeta = $formattedMetadata !== false; } else { - $exif = false; $showmeta = false; } @@ -76,12 +84,12 @@ class ImagePage extends Article { $this->imageHistory(); $this->imageLinks(); - if ( $exif ) { + if ( $showmeta ) { global $wgStylePath, $wgStyleVersion; $expand = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-expand' ) ) ); $collapse = htmlspecialchars( wfEscapeJsString( wfMsg( 'metadata-collapse' ) ) ); $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" ); - $wgOut->addWikiText( $this->makeMetadataTable( $exif ) ); + $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) ); $wgOut->addHTML( "\n" . "\n" ); @@ -100,9 +108,9 @@ class ImagePage extends Article { global $wgLang; $r = ''; return $r; } @@ -110,49 +118,31 @@ class ImagePage extends Article { /** * Make a table with metadata to be shown in the output page. * + * FIXME: bad interface, see note on MediaHandler::formatMetadata(). + * * @access private * * @param array $exif The array containing the EXIF data * @return string */ - function makeMetadataTable( $exif ) { + function makeMetadataTable( $metadata ) { $r = wfMsg( 'metadata-help' ) . "\n\n"; $r .= "{| id=mw_metadata class=mw_metadata\n"; - $visibleFields = $this->visibleMetadataFields(); - foreach( $exif as $k => $v ) { - $tag = strtolower( $k ); - $msg = wfMsg( "exif-$tag" ); - $class = "exif-$tag"; - if( !in_array( $tag, $visibleFields ) ) { - $class .= ' collapsable'; + foreach ( $metadata as $type => $stuff ) { + foreach ( $stuff as $v ) { + $class = Sanitizer::escapeId( $v['id'] ); + if( $type == 'collapsed' ) { + $class .= ' collapsable'; + } + $r .= "|- class=\"$class\"\n"; + $r .= "!| {$v['name']}\n"; + $r .= "|| {$v['value']}\n"; } - $r .= "|- class=\"$class\"\n"; - $r .= "!| $msg\n"; - $r .= "|| $v\n"; } $r .= '|}'; return $r; } - /** - * Get a list of EXIF metadata items which should be displayed when - * the metadata table is collapsed. - * - * @return array of strings - * @access private - */ - function visibleMetadataFields() { - $fields = array(); - $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) ); - foreach( $lines as $line ) { - $matches = array(); - if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { - $fields[] = $matches[1]; - } - } - return $fields; - } - /** * Overloading Article's getContent method. * @@ -160,7 +150,7 @@ class ImagePage extends Article { * shared upload server if possible. */ function getContent() { - if( $this->img && $this->img->fromSharedDirectory && 0 == $this->getID() ) { + if( $this->img && !$this->img->isLocal() && 0 == $this->getID() ) { return ''; } return Article::getContent(); @@ -203,12 +193,15 @@ class ImagePage extends Article { $mime = $this->img->getMimeType(); $showLink = false; $linkAttribs = array( 'href' => $full_url ); + $longDesc = $this->img->getLongDesc(); + + wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ; - if ( $this->img->allowInlineDisplay() and $width and $height) { + if ( $this->img->allowInlineDisplay() ) { # image # "Download high res version" link below the image - $msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime ); + #$msgsize = wfMsgHtml('file-info-size', $width_orig, $height_orig, $sk->formatSize( $this->img->getSize() ), $mime ); # We'll show a thumbnail of this image if ( $width > $maxWidth || $height > $maxHeight ) { # Calculate the thumbnail size. @@ -242,21 +235,20 @@ class ImagePage extends Article { } else { $anchorclose .= $msgsmall . - '
' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $msgsize; + '
' . Xml::tags( 'a', $linkAttribs, $msgbig ) . ' ' . $longDesc; } if ( $this->img->isMultipage() ) { $wgOut->addHTML( '' + . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '' : '' ) + . '' + . '' + . '' + . '' + . '' + . "\n"; + } - if ( $iscur ) { - $url = Image::imageUrl( $img ); - $rlink = $cur; - if ( $wgUser->isAllowed('delete') ) { - $link = $wgTitle->escapeLocalURL( 'image=' . $wgTitle->getPartialURL() . - '&action=delete' ); - $style = $this->skin->getInternalLinkAttributes( $link, $delall ); + public function endImageHistoryList() { + return "
' ); } - $imgAttribs = array( - 'border' => 0, - 'alt' => $this->img->getTitle()->getPrefixedText() - ); - if ( $thumbnail ) { + $options = array( + 'alt' => $this->img->getTitle()->getPrefixedText(), + 'file-link' => true, + ); $wgOut->addHTML( '' ); } @@ -265,8 +257,8 @@ class ImagePage extends Article { if ( $page > 1 ) { $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false ); - $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); - $thumb1 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none', + $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) ); + $thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none', array( 'page' => $page - 1 ) ); } else { $thumb1 = ''; @@ -274,8 +266,8 @@ class ImagePage extends Article { if ( $page < $count ) { $label = wfMsg( 'imgmultipagenext' ); - $link = $sk->makeLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); - $thumb2 = $sk->makeThumbLinkObj( $this->img, $link, $label, 'none', + $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) ); + $thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->img, $link, $label, 'none', array( 'page' => $page + 1 ) ); } else { $thumb2 = ''; @@ -304,9 +296,9 @@ class ImagePage extends Article { if ($this->img->isSafeFile()) { $icon= $this->img->iconThumb(); - $wgOut->addHTML( '' ); + $wgOut->addHTML( '' ); } $showLink = true; @@ -314,42 +306,32 @@ class ImagePage extends Article { if ($showLink) { - // Workaround for incorrect MIME type on SVGs uploaded in previous versions - if ($mime == 'image/svg') $mime = 'image/svg+xml'; - $filename = wfEscapeWikiText( $this->img->getName() ); - $info = wfMsg( 'file-info', $sk->formatSize( $this->img->getSize() ), $mime ); - $infores = ''; - - // Check for MIME type. Other types may have more information in the future. - if (substr($mime,0,9) == 'image/svg' ) { - $infores = wfMsg('file-svg', $width_orig, $height_orig ) . '
'; - } global $wgContLang; $dirmark = $wgContLang->getDirMark(); if (!$this->img->isSafeFile()) { $warning = wfMsg( 'mediawarning' ); - $wgOut->addWikiText( <<$infores + $wgOut->addWikiText( << [[Media:$filename|$filename]]$dirmark - $info + $longDesc
$warning
-END +EOT ); } else { - $wgOut->addWikiText( <<$infores -[[Media:$filename|$filename]]$dirmark $info + $wgOut->addWikiText( << +[[Media:$filename|$filename]]$dirmark $longDesc -END +EOT ); } } - if($this->img->fromSharedDirectory) { + if(!$this->img->isLocal()) { $this->printSharedImageText(); } } else { @@ -363,27 +345,21 @@ END } function printSharedImageText() { - global $wgRepositoryBaseUrl, $wgFetchCommonsDescriptions, $wgOut, $wgUser; - - $url = $wgRepositoryBaseUrl . urlencode($this->mTitle->getDBkey()); - $sharedtext = "
" . wfMsgWikiHtml("sharedupload"); - if ($wgRepositoryBaseUrl && !$wgFetchCommonsDescriptions) { + global $wgOut, $wgUser; + $descUrl = $this->img->getDescriptionUrl(); + $descText = $this->img->getDescriptionText(); + $s = "
" . wfMsgWikiHtml("sharedupload"); + if ( $descUrl && !$descText) { $sk = $wgUser->getSkin(); - $title = SpecialPage::getTitleFor( 'Upload' ); - $link = $sk->makeKnownLinkObj($title, wfMsgHtml('shareduploadwiki-linktext'), - array( 'wpDestFile' => urlencode( $this->img->getName() ))); - $sharedtext .= " " . wfMsgWikiHtml('shareduploadwiki', $link); + $link = $sk->makeExternalLink( $descUrl, wfMsg('shareduploadwiki-linktext') ); + $s .= " " . wfMsgWikiHtml('shareduploadwiki', $link); } - $sharedtext .= "
"; - $wgOut->addHTML($sharedtext); - - if ($wgRepositoryBaseUrl && $wgFetchCommonsDescriptions) { - $renderUrl = wfAppendQuery( $url, 'action=render' ); - wfDebug( "Fetching shared description from $renderUrl\n" ); - $text = Http::get( $renderUrl ); - if ($text) - $this->mExtraDescription = $text; + $s .= "
"; + $wgOut->addHTML($s); + + if ( $descText ) { + $this->mExtraDescription = $descText; } } @@ -400,7 +376,7 @@ END function uploadLinksBox() { global $wgUser, $wgOut; - if( $this->img->fromSharedDirectory ) + if( !$this->img->isLocal() ) return; $sk = $wgUser->getSkin(); @@ -408,7 +384,7 @@ END $wgOut->addHtml( '
    ' ); # "Upload a new version of this file" link - if( $wgUser->isAllowed( 'reupload' ) ) { + if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) { $ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) ); $wgOut->addHtml( "
  • " ); } @@ -439,25 +415,31 @@ END $line = $this->img->nextHistoryLine(); if ( $line ) { - $list = new ImageHistoryList( $sk ); + $list = new ImageHistoryList( $sk, $this->img ); + $file = $this->repo->newFileFromRow( $line ); + $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, - $line->img_width, $line->img_height + $dims ); while ( $line = $this->img->nextHistoryLine() ) { - $s .= $list->imageHistoryLine( false, $line->img_timestamp, - $line->oi_archive_name, $line->img_user, - $line->img_user_text, $line->img_size, $line->img_description, - $line->img_width, $line->img_height + $file = $this->repo->newFileFromRow( $line ); + $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, + $dims ); } $s .= $list->endImageHistoryList(); } else { $s=''; } $wgOut->addHTML( $s ); + $this->img->resetHistory(); // free db resources + # Exist check because we don't want to show this on pages where an image # doesn't exist along with the noimage message, that would suck. -ævar if( $wgUseExternalEditor && $this->img->exists() ) { @@ -496,207 +478,31 @@ END $wgOut->addHTML( "
\n" ); } - function delete() - { - global $wgUser, $wgOut, $wgRequest; - - $confirm = $wgRequest->wasPosted(); - $reason = $wgRequest->getVal( 'wpReason' ); - $image = $wgRequest->getVal( 'image' ); - $oldimage = $wgRequest->getVal( 'oldimage' ); - - # Only sysops can delete images. Previously ordinary users could delete - # old revisions, but this is no longer the case. - if ( !$wgUser->isAllowed('delete') ) { - $wgOut->permissionRequired( 'delete' ); - return; - } - if ( $wgUser->isBlocked() ) { - return $this->blockedIPpage(); - } - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } - - # Better double-check that it hasn't been deleted yet! - $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) ); - if ( ( !is_null( $image ) ) - && ( '' == trim( $image ) ) ) { - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); - return; - } - - $this->img = new Image( $this->mTitle ); - - # Deleting old images doesn't require confirmation - if ( !is_null( $oldimage ) || $confirm ) { - if( $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { - $this->doDelete( $reason ); - } else { - $wgOut->showFatalError( wfMsg( 'sessionfailure' ) ); - } - return; - } - - if ( !is_null( $image ) ) { - $q = '&image=' . urlencode( $image ); - } else if ( !is_null( $oldimage ) ) { - $q = '&oldimage=' . urlencode( $oldimage ); - } else { - $q = ''; - } - return $this->confirmDelete( $q, $wgRequest->getText( 'wpReason' ) ); - } - - /* - * Delete an image. - * @param $reason User provided reason for deletion. - */ - function doDelete( $reason ) { - global $wgOut, $wgRequest; - - $oldimage = $wgRequest->getVal( 'oldimage' ); - - if ( !is_null( $oldimage ) ) { - if ( strlen( $oldimage ) < 16 ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - if ( !$this->doDeleteOldImage( $oldimage ) ) { - return; - } - $deleted = $oldimage; - } else { - $ok = $this->img->delete( $reason ); - if( !$ok ) { - # If the deletion operation actually failed, bug out: - $wgOut->showFileDeleteError( $this->img->getName() ); - return; - } - - # Image itself is now gone, and database is cleaned. - # Now we remove the image description page. - - $article = new Article( $this->mTitle ); - $article->doDeleteArticle( $reason ); # ignore errors - - $deleted = $this->img->getName(); - } - - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - - $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]'; - $text = wfMsg( 'deletedtext', $deleted, $loglink ); - - $wgOut->addWikiText( $text ); - - $wgOut->returnToMain( false, $this->mTitle->getPrefixedText() ); - } - /** - * @return success + * Delete the file, or an earlier version of it */ - function doDeleteOldImage( $oldimage ) - { - global $wgOut; - - $ok = $this->img->deleteOld( $oldimage, '' ); - if( !$ok ) { - # If we actually have a file and can't delete it, throw an error. - # Something went awry... - $wgOut->showFileDeleteError( "$oldimage" ); - } else { - # Log the deletion - $log = new LogPage( 'delete' ); - $log->addEntry( 'delete', $this->mTitle, wfMsg('deletedrevision',$oldimage) ); - } - return $ok; - } - - function revert() { - global $wgOut, $wgRequest, $wgUser; - - $oldimage = $wgRequest->getText( 'oldimage' ); - if ( strlen( $oldimage ) < 16 ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - if ( strstr( $oldimage, "/" ) || strstr( $oldimage, "\\" ) ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars($oldimage) ); - return; - } - - if ( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } - if( $wgUser->isAnon() ) { - $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); + public function delete() { + if( !$this->img->exists() || !$this->img->isLocal() ) { + // Standard article deletion + Article::delete(); return; } - if ( ! $this->mTitle->userCan( 'edit' ) ) { - $wgOut->readOnlyPage( $this->getContent(), true ); - return; - } - if ( $wgUser->isBlocked() ) { - return $this->blockedIPpage(); - } - if( !$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ), $oldimage ) ) { - $wgOut->showErrorPage( 'internalerror', 'sessionfailure' ); - return; - } - $name = substr( $oldimage, 15 ); - - $dest = wfImageDir( $name ); - $archive = wfImageArchiveDir( $name ); - $curfile = "{$dest}/{$name}"; - - if ( !is_dir( $dest ) ) wfMkdirParents( $dest ); - if ( !is_dir( $archive ) ) wfMkdirParents( $archive ); - - if ( ! is_file( $curfile ) ) { - $wgOut->showFileNotFoundError( htmlspecialchars( $curfile ) ); - return; - } - $oldver = wfTimestampNow() . "!{$name}"; - - if ( ! rename( $curfile, "${archive}/{$oldver}" ) ) { - $wgOut->showFileRenameError( $curfile, "${archive}/{$oldver}" ); - return; - } - if ( ! copy( "{$archive}/{$oldimage}", $curfile ) ) { - $wgOut->showFileCopyError( "${archive}/{$oldimage}", $curfile ); - return; - } - - # Record upload and update metadata cache - $img = Image::newFromName( $name ); - $img->recordUpload( $oldver, wfMsg( "reverted" ) ); - - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - $wgOut->setRobotpolicy( 'noindex,nofollow' ); - $wgOut->addHTML( wfMsg( 'imagereverted' ) ); - - $descTitle = $img->getTitle(); - $wgOut->returnToMain( false, $descTitle->getPrefixedText() ); + $deleter = new FileDeleteForm( $this->img ); + $deleter->execute(); } - function blockedIPpage() { - $edit = new EditPage( $this ); - return $edit->blockedIPpage(); + /** + * Revert the file to an earlier version + */ + public function revert() { + $reverter = new FileRevertForm( $this->img ); + $reverter->execute(); } /** * Override handling of action=purge */ function doPurge() { - $this->img = new Image( $this->mTitle ); if( $this->img->exists() ) { wfDebug( "ImagePage::doPurge purging " . $this->img->getName() . "\n" ); $update = new HTMLCacheUpdate( $this->mTitle, 'imagelinks' ); @@ -709,82 +515,120 @@ END parent::doPurge(); } + /** + * Display an error with a wikitext description + */ + function showError( $description ) { + global $wgOut; + $wgOut->setPageTitle( wfMsg( "internalerror" ) ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->addWikiText( $description ); + } + } /** - * @todo document + * Builds the image revision log shown on image pages + * * @addtogroup Media */ class ImageHistoryList { - function ImageHistoryList( &$skin ) { - $this->skin =& $skin; - } - function beginImageHistoryList() { - $s = "\n" . - Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'imghistory' ) ) . - "\n

" . wfMsg( 'imghistlegend' ) . "

\n".'
    '; - return $s; - } + protected $img, $skin, $title, $repo; - function endImageHistoryList() { - $s = "
\n"; - return $s; + public function __construct( $skin, $img ) { + $this->skin = $skin; + $this->img = $img; + $this->title = $img->getTitle(); } - function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $width, $height ) { - global $wgUser, $wgLang, $wgTitle, $wgContLang; - - $datetime = $wgLang->timeanddate( $timestamp, true ); - $del = wfMsgHtml( 'deleteimg' ); - $delall = wfMsgHtml( 'deleteimgcompletely' ); - $cur = wfMsgHtml( 'cur' ); + public function beginImageHistoryList() { + global $wgOut, $wgUser; + return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) + . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) ) + . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n" + . '
' . wfMsgHtml( 'filehist-datetime' ) . '' . wfMsgHtml( 'filehist-user' ) . '' . wfMsgHtml( 'filehist-dimensions' ) . '' . wfMsgHtml( 'filehist-filesize' ) . '' . wfMsgHtml( 'filehist-comment' ) . '
\n"; + } - $dlink = ''.$delall.''; - } else { - $dlink = $del; - } + public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) { + global $wgUser, $wgLang, $wgContLang; + $local = $this->img->isLocal(); + $row = ''; + + // Deletion link + if( $local && $wgUser->isAllowed( 'delete' ) ) { + $row .= ''; + $q = array(); + $q[] = 'action=delete'; + if( !$iscur ) + $q[] = 'oldimage=' . urlencode( $img ); + $row .= '(' . $this->skin->makeKnownLinkObj( + $this->title, + wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ), + implode( '&', $q ) + ) . ')'; + $row .= ''; + } + + // Reversion link/current indicator + $row .= ''; + if( $iscur ) { + $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( + $this->title, + wfMsgHtml( 'filehist-revert' ), + implode( '&', $q ) + ) . ')'; + } + $row .= ''; + + // Date/time and image link + $row .= ''; + $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img ); + $row .= Xml::element( + 'a', + array( 'href' => $url ), + $wgLang->timeAndDate( $timestamp, true ) + ); + $row .= ''; + + // Uploading user + $row .= ''; + if( $local ) { + $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); } else { - $url = htmlspecialchars( wfImageArchiveUrl( $img ) ); - if( $wgUser->getID() != 0 && $wgTitle->userCan( 'edit' ) ) { - $token = urlencode( $wgUser->editToken( $img ) ); - $rlink = $this->skin->makeKnownLinkObj( $wgTitle, - wfMsgHtml( 'revertimg' ), 'action=revert&oldimage=' . - urlencode( $img ) . "&wpEditToken=$token" ); - $dlink = $this->skin->makeKnownLinkObj( $wgTitle, - $del, 'action=delete&oldimage=' . urlencode( $img ) . - "&wpEditToken=$token" ); - } else { - # Having live active links for non-logged in users - # means that bots and spiders crawling our site can - # inadvertently change content. Baaaad idea. - $rlink = wfMsgHtml( 'revertimg' ); - $dlink = $del; - } + $row .= htmlspecialchars( $usertext ); } - - $userlink = $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext ); - $nbytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), - $wgLang->formatNum( $size ) ); - $widthheight = wfMsgHtml( 'widthheight', $width, $height ); - $style = $this->skin->getInternalLinkAttributes( $url, $datetime ); + $row .= ''; - $s = "
  • ({$dlink}) ({$rlink}) {$datetime} . . {$userlink} . . {$widthheight} ({$nbytes})"; + // Image dimensions + $row .= '' . htmlspecialchars( $dims ) . ''; - $s .= $this->skin->commentBlock( $description, $wgTitle ); - $s .= "
  • \n"; - return $s; - } + // File size + $row .= '' . $this->skin->formatSize( $size ) . ''; -} + // Comment + $row .= '' . $this->skin->formatComment( $description, $this->title ) . ''; + return "{$row}\n"; + } -?> +} diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php index 93f090a1..8948ddc6 100644 --- a/includes/ImageQueryPage.php +++ b/includes/ImageQueryPage.php @@ -30,8 +30,8 @@ class ImageQueryPage extends QueryPage { # $num [should update this to use a Pager] for( $i = 0; $i < $num && $row = $dbr->fetchObject( $res ); $i++ ) { $image = $this->prepareImage( $row ); - if( $image instanceof Image ) { - $gallery->add( $image, $this->getCellHtml( $row ) ); + if( $image ) { + $gallery->add( $image->getTitle(), $this->getCellHtml( $row ) ); } } @@ -49,7 +49,7 @@ class ImageQueryPage extends QueryPage { $namespace = isset( $row->namespace ) ? $row->namespace : NS_IMAGE; $title = Title::makeTitleSafe( $namespace, $row->title ); return ( $title instanceof Title && $title->getNamespace() == NS_IMAGE ) - ? new Image( $title ) + ? wfFindFile( $title ) : null; } @@ -65,4 +65,4 @@ class ImageQueryPage extends QueryPage { } -?> + diff --git a/includes/JobQueue.php b/includes/JobQueue.php index 140130fa..a2780bdb 100644 --- a/includes/JobQueue.php +++ b/includes/JobQueue.php @@ -4,6 +4,8 @@ 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. */ @@ -36,6 +38,46 @@ abstract class Job { * static function queueLinksJobs( $titles ) {} */ + /** + * Pop a job of a certain type. This tries less hard than pop() to + * actually find a job; it may be adversely affected by concurrent job + * runners. + */ + static function pop_type($type) { + wfProfilein( __METHOD__ ); + + $dbw = wfGetDB( DB_MASTER ); + + + $row = $dbw->selectRow( 'job', '*', array( 'job_cmd' => $type ), __METHOD__, + array( 'LIMIT' => 1 )); + + if ($row === false) { + wfProfileOut( __METHOD__ ); + return false; + } + + /* Ensure we "own" this row */ + $dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ ); + $affected = $dbw->affectedRows(); + + if ($affected == 0) { + wfProfileOut( __METHOD__ ); + return false; + } + + $namespace = $row->job_namespace; + $dbkey = $row->job_title; + $title = Title::makeTitleSafe( $namespace, $dbkey ); + $job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id ); + + $dbw->delete( 'job', $job->insertFields(), __METHOD__ ); + $dbw->immediateCommit(); + + wfProfileOut( __METHOD__ ); + return $job; + } + /** * Pop a job off the front of the queue * @static @@ -125,20 +167,23 @@ abstract class Job { } /** - * Create an object of a subclass + * Create the appropriate object to handle a specific job + * + * @param string $command Job command + * @param Title $title Associated title + * @param array $params Job parameters + * @param int $id Job identifier + * @return Job */ static function factory( $command, $title, $params = false, $id = 0 ) { - switch ( $command ) { - case 'refreshLinks': - return new RefreshLinksJob( $title, $params, $id ); - case 'htmlCacheUpdate': - case 'html_cache_update': # BC - return new HTMLCacheUpdateJob( $title, $params['table'], $params['start'], $params['end'], $id ); - default: - throw new MWException( "Invalid job command \"$command\"" ); + global $wgJobClasses; + if( isset( $wgJobClasses[$command] ) ) { + $class = $wgJobClasses[$command]; + return new $class( $title, $params, $id ); } + throw new MWException( "Invalid job command `{$command}`" ); } - + static function makeBlob( $params ) { if ( $params !== false ) { return serialize( $params ); @@ -245,50 +290,3 @@ abstract class Job { } } - -/** - * Background job to update links for a given title. - */ -class RefreshLinksJob extends Job { - function __construct( $title, $params = '', $id = 0 ) { - parent::__construct( 'refreshLinks', $title, $params, $id ); - } - - /** - * Run a refreshLinks job - * @return boolean success - */ - function run() { - global $wgParser; - wfProfileIn( __METHOD__ ); - - $linkCache =& LinkCache::singleton(); - $linkCache->clear(); - - if ( is_null( $this->title ) ) { - $this->error = "refreshLinks: Invalid title"; - wfProfileOut( __METHOD__ ); - return false; - } - - $revision = Revision::newFromTitle( $this->title ); - if ( !$revision ) { - $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"'; - wfProfileOut( __METHOD__ ); - return false; - } - - wfProfileIn( __METHOD__.'-parse' ); - $options = new ParserOptions; - $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() ); - wfProfileOut( __METHOD__.'-parse' ); - wfProfileIn( __METHOD__.'-update' ); - $update = new LinksUpdate( $this->title, $parserOutput, false ); - $update->doUpdate(); - wfProfileOut( __METHOD__.'-update' ); - wfProfileOut( __METHOD__ ); - return true; - } -} - -?> diff --git a/includes/Licenses.php b/includes/Licenses.php index f4586ae5..6a034468 100644 --- a/includes/Licenses.php +++ b/includes/Licenses.php @@ -172,4 +172,4 @@ class License { $this->text = strrev( $text ); } } -?> + diff --git a/includes/LinkBatch.php b/includes/LinkBatch.php index 065c540a..8ab3393e 100644 --- a/includes/LinkBatch.php +++ b/includes/LinkBatch.php @@ -3,7 +3,7 @@ /** * Class representing a list of titles * The execute() method checks them all for existence and adds them to a LinkCache object - + + * * @addtogroup Cache */ class LinkBatch { @@ -156,19 +156,26 @@ class LinkBatch { } else { $sql .= ' OR '; } - $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN ("; - - $firstTitle = true; - foreach( $dbkeys as $dbkey => $unused ) { - if ( $firstTitle ) { - $firstTitle = false; - } else { - $sql .= ','; + + if (count($dbkeys)==1) { // avoid multiple-reference syntax if simple equality can be used + + $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title=". + $db->addQuotes(current(array_keys($dbkeys))). + ")"; + } else { + $sql .= "({$prefix}_namespace=$ns AND {$prefix}_title IN ("; + + $firstTitle = true; + foreach( $dbkeys as $dbkey => $unused ) { + if ( $firstTitle ) { + $firstTitle = false; + } else { + $sql .= ','; + } + $sql .= $db->addQuotes( $dbkey ); } - $sql .= $db->addQuotes( $dbkey ); + $sql .= '))'; } - - $sql .= '))'; } if ( $first && $firstTitle ) { # No titles added @@ -179,4 +186,4 @@ class LinkBatch { } } -?> + diff --git a/includes/LinkCache.php b/includes/LinkCache.php index 53fb640a..7c49d88e 100644 --- a/includes/LinkCache.php +++ b/includes/LinkCache.php @@ -169,4 +169,4 @@ class LinkCache { $this->mBadLinks = array(); } } -?> + diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php index 39341d5d..ee668f08 100644 --- a/includes/LinkFilter.php +++ b/includes/LinkFilter.php @@ -105,4 +105,4 @@ class LinkFilter { return $like; } } -?> + diff --git a/includes/Linker.php b/includes/Linker.php index b12e2ad0..9397b800 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -12,6 +12,12 @@ * @addtogroup Skins */ class Linker { + + /** + * Flags for userToolLinks() + */ + const TOOL_LINKS_NOBLOCK = 1; + function __construct() {} /** @@ -96,7 +102,7 @@ class Linker { wfProfileIn( 'Linker::makeLink' ); $nt = Title::newFromText( $title ); if ($nt) { - $result = $this->makeLinkObj( Title::newFromText( $title ), $text, $query, $trail ); + $result = $this->makeLinkObj( $nt, $text, $query, $trail ); } else { wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" ); $result = $text == "" ? $title : $text; @@ -218,32 +224,39 @@ class Linker { $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); } else { wfProfileIn( $fname.'-immediate' ); + + # Handles links to special pages wich do not exist in the database: + if( $nt->getNamespace() == NS_SPECIAL ) { + 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 ); + return $retVal; + } + # Work out link colour immediately $aid = $nt->getArticleID() ; if ( 0 == $aid ) { $retVal = $this->makeBrokenLinkObj( $nt, $text, $query, $trail, $prefix ); } else { - $threshold = $wgUser->getOption('stubthreshold') ; - if ( $threshold > 0 ) { - $dbr = wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( - array( 'page' ), - array( 'page_len', - 'page_namespace', - 'page_is_redirect' ), - array( 'page_id' => $aid ), $fname ) ; - if ( $s !== false ) { - $size = $s->page_len; - if ( $s->page_is_redirect OR $s->page_namespace != NS_MAIN ) { - $size = $threshold*2 ; # Really big - } - } else { - $size = $threshold*2 ; # Really big + $stub = false; + if ( $nt->isContentPage() ) { + $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 ); } - } else { - $size = 1 ; } - if ( $size < $threshold ) { + if ( $stub ) { $retVal = $this->makeStubLinkObj( $nt, $text, $query, $trail, $prefix ); } else { $retVal = $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix ); @@ -324,7 +337,9 @@ class Linker { $fname = 'Linker::makeBrokenLinkObj'; wfProfileIn( $fname ); - if ( '' == $query ) { + if( $nt->getNamespace() == NS_SPECIAL ) { + $q = $query; + } else if ( '' == $query ) { $q = 'action=edit'; } else { $q = 'action=edit&'.$query; @@ -418,42 +433,118 @@ class Linker { return $s; } - /** @todo document */ - function makeImageLinkObj( $nt, $label, $alt, $align = '', $params = array(), $framed = false, - $thumb = false, $manual_thumb = '', $valign = '' ) + /** + * Creates the HTML source for images + * @deprecated use makeImageLink2 + * + * @param object $title + * @param string $label label text + * @param string $alt alt text + * @param string $align horizontal alignment: none, left, center, right) + * @param array $handlerParams Parameters to be passed to the media handler + * @param boolean $framed shows image in original size in a frame + * @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 + * @return string + */ + function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false, + $thumb = false, $manualthumb = '', $valign = '', $time = false ) { - global $wgContLang, $wgUser, $wgThumbLimits; - - $img = new Image( $nt ); + $frameParams = array( 'alt' => $alt, 'caption' => $label ); + if ( $align ) { + $frameParams['align'] = $align; + } + if ( $framed ) { + $frameParams['framed'] = true; + } + if ( $thumb ) { + $frameParams['thumbnail'] = true; + } + if ( $manualthumb ) { + $frameParams['manualthumb'] = $manualthumb; + } + if ( $valign ) { + $frameParams['valign'] = $valign; + } + $file = wfFindFile( $title, $time ); + return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams ); + } - if ( !$img->allowInlineDisplay() && $img->exists() ) { - return $this->makeKnownLinkObj( $nt ); + /** + * Make an image link + * @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 $handlerParams Associative array of media handler parameters, to be passed + * to transform(). Typical keys are "width" and "page". + */ + function makeImageLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) { + global $wgContLang, $wgUser, $wgThumbLimits, $wgThumbUpright; + if ( $file && !$file->allowInlineDisplay() ) { + wfDebug( __METHOD__.': '.$title->getPrefixedDBkey()." does not allow inline display\n" ); + return $this->makeKnownLinkObj( $title ); } - $error = $prefix = $postfix = ''; - $page = isset( $params['page'] ) ? $params['page'] : false; + // Shortcuts + $fp =& $frameParams; + $hp =& $handlerParams; - if ( 'center' == $align ) + // Clean up parameters + $page = isset( $hp['page'] ) ? $hp['page'] : false; + if ( !isset( $fp['align'] ) ) $fp['align'] = ''; + if ( !isset( $fp['alt'] ) ) $fp['alt'] = ''; + + $prefix = $postfix = ''; + + if ( 'center' == $fp['align'] ) { $prefix = '
    '; $postfix = '
    '; - $align = 'none'; + $fp['align'] = 'none'; } + if ( $file && !isset( $hp['width'] ) ) { + $hp['width'] = $file->getWidth( $page ); - if ( !isset( $params['width'] ) ) { - $params['width'] = $img->getWidth( $page ); - if( $thumb || $framed ) { + if( isset( $fp['thumbnail'] ) || isset( $fp['framed'] ) || isset( $fp['frameless'] ) || !$hp['width'] ) { $wopt = $wgUser->getOption( 'thumbsize' ); if( !isset( $wgThumbLimits[$wopt] ) ) { $wopt = User::getDefaultOption( 'thumbsize' ); } - $params['width'] = min( $params['width'], $wgThumbLimits[$wopt] ); + // Reduce width for upright images when parameter 'upright' is used + if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) { + $fp['upright'] = $wgThumbUpright; + } + // Use width which is smaller: real image width or user preference width + // For caching health: If width scaled down due to upright parameter, round to full __0 pixel to avoid the creation of a lot of odd thumbs + $prefWidth = isset( $fp['upright'] ) ? + round( $wgThumbLimits[$wopt] * $fp['upright'], -1 ) : + $wgThumbLimits[$wopt]; + if ( $hp['width'] <= 0 || $prefWidth < $hp['width'] ) { + $hp['width'] = $prefWidth; + } } } - if ( $thumb || $framed ) { + if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) { # Create a thumbnail. Alignment depends on language # writing direction, # right aligned for left-to-right- @@ -462,159 +553,174 @@ class Linker { # # If thumbnail width has not been provided, it is set # to the default user option as specified in Language*.php - if ( $align == '' ) { - $align = $wgContLang->isRTL() ? 'left' : 'right'; + if ( $fp['align'] == '' ) { + $fp['align'] = $wgContLang->isRTL() ? 'left' : 'right'; } - return $prefix.$this->makeThumbLinkObj( $img, $label, $alt, $align, $params, $framed, $manual_thumb ).$postfix; + return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp ).$postfix; } - if ( $params['width'] && $img->exists() ) { + if ( $file && $hp['width'] ) { # Create a resized image, without the additional thumbnail features - $thumb = $img->transform( $params ); + $thumb = $file->transform( $hp ); } else { $thumb = false; } - if ( $page ) { - $query = 'page=' . urlencode( $page ); - } else { - $query = ''; - } - $u = $nt->getLocalURL( $query ); - $imgAttribs = array( - 'alt' => $alt, - 'longdesc' => $u - ); - if ( $valign ) { - $imgAttribs['style'] = "vertical-align: $valign"; - } - $linkAttribs = array( - 'href' => $u, - 'class' => 'image', - 'title' => $alt - ); - if ( !$thumb ) { - $s = $this->makeBrokenImageLinkObj( $img->getTitle() ); + $s = $this->makeBrokenImageLinkObj( $title ); } else { - $s = $thumb->toHtml( $imgAttribs, $linkAttribs ); + $s = $thumb->toHtml( array( + 'desc-link' => true, + 'alt' => $fp['alt'], + 'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false , + 'img-class' => isset( $fp['border'] ) ? 'thumbborder' : false ) ); } - if ( '' != $align ) { - $s = "
    {$s}
    "; + if ( '' != $fp['align'] ) { + $s = "
    {$s}
    "; } return str_replace("\n", ' ',$prefix.$s.$postfix); } /** * Make HTML for a thumbnail including image, border and caption - * $img is an Image object + * @param Title $title + * @param File $file File object or false if it doesn't exist */ - function makeThumbLinkObj( $img, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manual_thumb = "" ) { + function makeThumbLinkObj( Title $title, $file, $label = '', $alt, $align = 'right', $params = array(), $framed=false , $manualthumb = "" ) { + $frameParams = array( + 'alt' => $alt, + 'caption' => $label, + 'align' => $align + ); + if ( $framed ) $frameParams['framed'] = true; + if ( $manualthumb ) $frameParams['manualthumb'] = $manualthumb; + return $this->makeThumbLink2( $title, $file, $frameParams, $params ); + } + + function makeThumbLink2( Title $title, $file, $frameParams = array(), $handlerParams = array() ) { global $wgStylePath, $wgContLang; - $thumbUrl = ''; - $error = ''; + $exists = $file && $file->exists(); + + # Shortcuts + $fp =& $frameParams; + $hp =& $handlerParams; - $page = isset( $params['page'] ) ? $params['page'] : false; + $page = isset( $hp['page'] ) ? $hp['page'] : false; + if ( !isset( $fp['align'] ) ) $fp['align'] = 'right'; + if ( !isset( $fp['alt'] ) ) $fp['alt'] = ''; + if ( !isset( $fp['caption'] ) ) $fp['caption'] = ''; - if ( empty( $params['width'] ) ) { - $params['width'] = 180; + if ( empty( $hp['width'] ) ) { + // Reduce width for upright images when parameter 'upright' is used + $hp['width'] = isset( $fp['upright'] ) ? 130 : 180; } $thumb = false; - if ( $manual_thumb != '' ) { - # Use manually specified thumbnail - $manual_title = Title::makeTitleSafe( NS_IMAGE, $manual_thumb ); - if( $manual_title ) { - $manual_img = new Image( $manual_title ); - $thumb = $manual_img->getUnscaledThumb(); - } - } elseif ( $framed ) { - // Use image dimensions, don't scale - $thumb = $img->getUnscaledThumb( $page ); - } else { - $thumb = $img->transform( $params ); - } - if ( $thumb ) { - $outerWidth = $thumb->getWidth() + 2; + if ( !$exists ) { + $outerWidth = $hp['width'] + 2; } else { - $outerWidth = $params['width'] + 2; + if ( isset( $fp['manualthumb'] ) ) { + # Use manually specified thumbnail + $manual_title = Title::makeTitleSafe( NS_IMAGE, $fp['manualthumb'] ); + if( $manual_title ) { + $manual_img = wfFindFile( $manual_title ); + if ( $manual_img ) { + $thumb = $manual_img->getUnscaledThumb(); + } else { + $exists = false; + } + } + } elseif ( isset( $fp['framed'] ) ) { + // Use image dimensions, don't scale + $thumb = $file->getUnscaledThumb( $page ); + } else { + # Do not present an image bigger than the source, for bitmap-style images + # This is a hack to maintain compatibility with arbitrary pre-1.10 behaviour + $srcWidth = $file->getWidth( $page ); + if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) { + $hp['width'] = $srcWidth; + } + $thumb = $file->transform( $hp ); + } + + if ( $thumb ) { + $outerWidth = $thumb->getWidth() + 2; + } else { + $outerWidth = $hp['width'] + 2; + } } $query = $page ? 'page=' . urlencode( $page ) : ''; - $u = $img->getTitle()->getLocalURL( $query ); + $url = $title->getLocalURL( $query ); $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); $magnifyalign = $wgContLang->isRTL() ? 'left' : 'right'; $textalign = $wgContLang->isRTL() ? ' style="text-align:right"' : ''; - $s = "
    "; - if ( !$thumb ) { - $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); + $s = "
    "; + if( !$exists ) { + $s .= $this->makeBrokenImageLinkObj( $title ); $zoomicon = ''; - } elseif( !$img->exists() ) { - $s .= $this->makeBrokenImageLinkObj( $img->getTitle() ); + } elseif ( !$thumb ) { + $s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) ); $zoomicon = ''; } else { - $imgAttribs = array( - 'alt' => $alt, - 'longdesc' => $u, - 'class' => 'thumbimage' - ); - $linkAttribs = array( - 'href' => $u, - 'class' => 'internal', - 'title' => $alt - ); - - $s .= $thumb->toHtml( $imgAttribs, $linkAttribs ); - if ( $framed ) { + $s .= $thumb->toHtml( array( + 'alt' => $fp['alt'], + 'img-class' => 'thumbimage', + 'desc-link' => true ) ); + if ( isset( $fp['framed'] ) ) { $zoomicon=""; } else { $zoomicon = ''; } } - $s .= '
    '.$zoomicon.$label."
    "; + $s .= '
    '.$zoomicon.$fp['caption']."
    "; return str_replace("\n", ' ', $s); } /** - * Pass a title object, not a title string + * Make a "broken" link to an image + * + * @param Title $title Image title + * @param string $text Link label + * @param string $query Query string + * @param string $trail Link trail + * @param string $prefix Link prefix + * @return string */ - function makeBrokenImageLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) { - # Fail gracefully - if ( ! isset($nt) ) { - # throw new MWException(); + public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) { + global $wgEnableUploads; + if( $title instanceof Title ) { + wfProfileIn( __METHOD__ ); + if( $wgEnableUploads ) { + $upload = SpecialPage::getTitleFor( 'Upload' ); + if( $text == '' ) + $text = htmlspecialchars( $title->getPrefixedText() ); + $q = 'wpDestFile=' . $title->getPartialUrl(); + if( $query != '' ) + $q .= '&' . $query; + list( $inside, $trail ) = self::splitTrail( $trail ); + $style = $this->getInternalLinkAttributesObj( $title, $text, 'yes' ); + wfProfileOut( __METHOD__ ); + return '' . $prefix . $text . $inside . '' . $trail; + } else { + wfProfileOut( __METHOD__ ); + return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix ); + } + } else { return "{$prefix}{$text}{$trail}"; } - - $fname = 'Linker::makeBrokenImageLinkObj'; - wfProfileIn( $fname ); - - $q = 'wpDestFile=' . urlencode( $nt->getDBkey() ); - if ( '' != $query ) { - $q .= "&$query"; - } - $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); - $url = $uploadTitle->escapeLocalURL( $q ); - - if ( '' == $text ) { - $text = htmlspecialchars( $nt->getPrefixedText() ); - } - $style = $this->getInternalLinkAttributesObj( $nt, $text, "yes" ); - list( $inside, $trail ) = Linker::splitTrail( $trail ); - $s = "{$prefix}{$text}{$inside}{$trail}"; - - wfProfileOut( $fname ); - return $s; } - /** @todo document */ - function makeMediaLink( $name, /* wtf?! */ $url, $alt = '' ) { + /** @deprecated use Linker::makeMediaLinkObj() */ + function makeMediaLink( $name, $unused = '', $text = '' ) { $nt = Title::makeTitleSafe( NS_IMAGE, $name ); - return $this->makeMediaLinkObj( $nt, $alt ); + return $this->makeMediaLinkObj( $nt, $text ); } /** @@ -632,13 +738,13 @@ class Linker { ### HOTFIX. Instead of breaking, return empty string. return $text; } else { - $img = new Image( $title ); - if( $img->exists() ) { + $img = wfFindFile( $title ); + if( $img ) { $url = $img->getURL(); $class = 'internal'; } else { $upload = SpecialPage::getTitleFor( 'Upload' ); - $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $img->getName() ) ); + $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDbKey() ) ); $class = 'new'; } $alt = htmlspecialchars( $title->getText() ); @@ -694,15 +800,18 @@ class Linker { } /** - * @param $userId Integer: user id in database. - * @param $userText String: user name in database. - * @param $redContribsWhenNoEdits Bool: return a red contribs link when the user had no edits and this is true. - * @return string HTML fragment with talk and/or block links + * Generate standard user tool links (talk, contributions, block link, etc.) + * + * @param int $userId User identifier + * @param string $userText User name or IP address + * @param bool $redContribsWhenNoEdits Should the contributions link be red if the user has no edits? + * @param int $flags Customisation flags (e.g. self::TOOL_LINKS_NOBLOCK) + * @return string */ - public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false ) { + public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0 ) { global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans; $talkable = !( $wgDisableAnonTalk && 0 == $userId ); - $blockable = ( $wgSysopUserBans || 0 == $userId ); + $blockable = ( $wgSysopUserBans || 0 == $userId ) && !$flags & self::TOOL_LINKS_NOBLOCK; $items = array(); if( $talkable ) { @@ -711,7 +820,7 @@ class Linker { if( $userId ) { // check if the user has an edit if( $redContribsWhenNoEdits && User::edits( $userId ) == 0 ) { - $style = "class='new'"; + $style = " class='new'"; } else { $style = ''; } @@ -818,16 +927,33 @@ class Linker { function formatComment($comment, $title = NULL, $local = false) { wfProfileIn( __METHOD__ ); - global $wgContLang; + # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); $comment = htmlspecialchars( $comment ); - # The pattern for autogen comments is / * foo * /, which makes for - # some nasty regex. - # We look for all comments, match any text before and after the comment, - # add a separator where needed and format the comment itself with CSS + # Render autocomments and make links: + $comment = $this->formatAutoComments( $comment, $title, $local ); + $comment = $this->formatLinksInComment( $comment ); + + wfProfileOut( __METHOD__ ); + return $comment; + } + + /** + * The pattern for autogen comments is / * foo * /, which makes for + * some nasty regex. + * We look for all comments, match any text before and after the comment, + * 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 + * + * @todo Document the $local parameter. + */ + private function formatAutocomments( $comment, $title = NULL, $local = false ) { $match = array(); - while (preg_match('/(.*)\/\*\s*(.*?)\s*\*\/(.*)/', $comment,$match)) { + while (preg_match('!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment,$match)) { $pre=$match[1]; $auto=$match[2]; $post=$match[3]; @@ -859,10 +985,23 @@ class Linker { $comment=$pre.$auto.$post; } - # format regular and media links - all other wiki formatting - # is ignored + return $comment; + } + + /** + * Formats wiki links and media links in text; all other wiki formatting + * is ignored + * + * @param string $comment Text to format links in + * @return string + */ + public function formatLinksInComment( $comment ) { + global $wgContLang; + $medians = '(?:' . preg_quote( Namespace::getCanonicalName( NS_MEDIA ), '/' ) . '|'; $medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):'; + + $match = array(); while(preg_match('/\[\[:?(.*?)(\|(.*?))*\]\](.*)$/',$comment,$match)) { # Handle link renaming [[foo|text]] will show link as "text" if( "" != $match[3] ) { @@ -889,7 +1028,7 @@ class Linker { } $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 ); } - wfProfileOut( __METHOD__ ); + return $comment; } @@ -914,7 +1053,7 @@ class Linker { return " ($formatted)"; } } - + /** * Wrap and format the given revision's comment block, if the current * user is allowed to view it. @@ -981,31 +1120,66 @@ class Linker { . "\n"; } - /** @todo document */ + /** + * Used to generate section edit links that point to "other" pages + * (sections that are really part of included pages). + * + * @param $title Title string. + * @param $section Integer: section number. + */ public function editSectionLinkForOther( $title, $section ) { - global $wgContLang; - $title = Title::newFromText( $title ); - $editurl = '§ion='.$section; - $url = $this->makeKnownLinkObj( $title, wfMsg('editsection'), 'action=edit'.$editurl ); - - return "[".$url."]"; - + return $this->doEditSectionLink( $title, $section, '', 'EditSectionLinkForOther' ); } /** - * @param $title Title object. + * @param $nt Title object. * @param $section Integer: section number. * @param $hint Link String: title, or default if omitted or empty */ - public function editSectionLink( $nt, $section, $hint='' ) { - global $wgContLang; + public function editSectionLink( Title $nt, $section, $hint='' ) { + if( $hint != '' ) { + $hint = wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ); + $hint = " title=\"$hint\""; + } + return $this->doEditSectionLink( $nt, $section, $hint, 'EditSectionLink' ); + } + /** + * Implement editSectionLink and editSectionLinkForOther. + * + * @param $nt Title object + * @param $section Integer, section number + * @param $hint String, for HTML title attribute + * @param $hook String, name of hook to run + * @return String, HTML to use for edit link + */ + protected function doEditSectionLink( Title $nt, $section, $hint, $hook ) { + global $wgContLang; $editurl = '§ion='.$section; - $hint = ( $hint=='' ) ? '' : ' title="' . wfMsgHtml( 'editsectionhint', htmlspecialchars( $hint ) ) . '"'; - $url = $this->makeKnownLinkObj( $nt, wfMsg('editsection'), 'action=edit'.$editurl, '', '', '', $hint ); + $url = $this->makeKnownLinkObj( + $nt, + wfMsg('editsection'), + 'action=edit'.$editurl, + '', '', '', $hint + ); + $result = null; - return "[".$url."]"; + // The two hooks have slightly different interfaces . . . + if( $hook == 'EditSectionLink' ) { + wfRunHooks( $hook, array( &$this, $nt, $section, $hint, $url, &$result ) ); + } elseif( $hook == 'EditSectionLinkForOther' ) { + wfRunHooks( $hook, array( &$this, $nt, $section, $url, &$result ) ); + } + + // For reverse compatibility, add the brackets *after* the hook is run, + // and even add them to hook-provided text. + if( is_null( $result ) ) { + $result = wfMsg( 'editsection-brackets', $url ); + } else { + $result = wfMsg( 'editsection-brackets', $result ); + } + return "$result"; } /** @@ -1061,15 +1235,28 @@ class Linker { * @param Revision $rev */ function generateRollback( $rev ) { - global $wgUser, $wgRequest; + return '[' + . $this->buildRollbackLink( $rev ) + . ']'; + } + + /** + * Build a raw rollback link, useful for collections of "tool" links + * + * @param Revision $rev + * @return string + */ + public function buildRollbackLink( $rev ) { + global $wgRequest, $wgUser; $title = $rev->getTitle(); - - $extraRollback = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; - $extraRollback .= '&token=' . urlencode( - $wgUser->editToken( array( $title->getPrefixedText(), $rev->getUserText() ) ) ); - return '['. $this->makeKnownLinkObj( $title, - wfMsg('rollbacklink'), - 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extraRollback ) .']'; + $extra = $wgRequest->getBool( 'bot' ) ? '&bot=1' : ''; + $extra .= '&token=' . urlencode( $wgUser->editToken( array( $title->getPrefixedText(), + $rev->getUserText() ) ) ); + return $this->makeKnownLinkObj( + $title, + wfMsgHtml( 'rollbacklink' ), + 'action=rollback&from=' . urlencode( $rev->getUserText() ) . $extra + ); } /** @@ -1133,28 +1320,7 @@ class Linker { */ public function formatSize( $size ) { global $wgLang; - // For small sizes no decimal places necessary - $round = 0; - if( $size > 1024 ) { - $size = $size / 1024; - if( $size > 1024 ) { - $size = $size / 1024; - // For MB and bigger two decimal places are smarter - $round = 2; - if( $size > 1024 ) { - $size = $size / 1024; - $msg = 'size-gigabytes'; - } else { - $msg = 'size-megabytes'; - } - } else { - $msg = 'size-kilobytes'; - } - } else { - $msg = 'size-bytes'; - } - $size = round( $size, $round ); - return wfMsgHtml( $msg, $wgLang->formatNum( $size ) ); + return htmlspecialchars( $wgLang->formatSize( $size ) ); } /** @@ -1208,4 +1374,6 @@ class Linker { } } -?> + + + diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 856c665d..9bcd9d67 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -9,7 +9,7 @@ class LinksUpdate { /**@{{ * @private */ - var $mId, //!< Page ID of the article linked from + var $mId, //!< Page ID of the article linked from $mTitle, //!< Title object of the article linked from $mLinks, //!< Map of title strings to IDs for the links in the document $mImages, //!< DB keys of the images used, in the array key only @@ -24,10 +24,10 @@ class LinksUpdate { /** * Constructor - * Initialize private variables - * @param $title Integer: FIXME - * @param $parserOutput FIXME - * @param $recursive Boolean: FIXME, default 'true'. + * + * @param Title $title Title of the page we're updating + * @param ParserOutput $parserOutput Output from a full parse of this page + * @param bool $recursive Queue jobs for recursive updates? */ function LinksUpdate( $title, $parserOutput, $recursive = true ) { global $wgAntiLockFlags; @@ -64,6 +64,8 @@ class LinksUpdate { } $this->mRecursive = $recursive; + + wfRunHooks( 'LinksUpdateConstructed', array( &$this ) ); } /** @@ -188,7 +190,7 @@ class LinksUpdate { break; } $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $jobs[] = Job::factory( 'refreshLinks', $title ); + $jobs[] = new RefreshLinksJob( $title, '' ); } Job::batchInsert( $jobs ); } @@ -594,4 +596,4 @@ class LinksUpdate { return $arr; } } -?> + diff --git a/includes/LoadBalancer.php b/includes/LoadBalancer.php index 4ebe26c7..65a6d5a6 100644 --- a/includes/LoadBalancer.php +++ b/includes/LoadBalancer.php @@ -646,4 +646,4 @@ class LoadBalancer { } } -?> + diff --git a/includes/LogPage.php b/includes/LogPage.php index af03bbba..8982b59f 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -55,42 +55,52 @@ class LogPage { $dbw = wfGetDB( DB_MASTER ); $uid = $wgUser->getID(); + $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' ); $this->timestamp = $now = wfTimestampNow(); - $dbw->insert( 'logging', - array( - 'log_type' => $this->type, - 'log_action' => $this->action, - 'log_timestamp' => $dbw->timestamp( $now ), - 'log_user' => $uid, - 'log_namespace' => $this->target->getNamespace(), - 'log_title' => $this->target->getDBkey(), - 'log_comment' => $this->comment, - 'log_params' => $this->params - ), $fname + $data = array( + 'log_type' => $this->type, + 'log_action' => $this->action, + 'log_timestamp' => $dbw->timestamp( $now ), + 'log_user' => $uid, + 'log_namespace' => $this->target->getNamespace(), + 'log_title' => $this->target->getDBkey(), + 'log_comment' => $this->comment, + 'log_params' => $this->params ); + # log_id doesn't exist on Wikimedia servers yet, and it's a tricky + # schema update to do. Hack it for now to ignore the field on MySQL. + if ( !is_null( $log_id ) ) { + $data['log_id'] = $log_id; + } + $dbw->insert( 'logging', $data, $fname ); + # And update recentchanges if ( $this->updateRecentChanges ) { $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); - $rcComment = $this->actionText; - if( '' != $this->comment ) { - if ($rcComment == '') - $rcComment = $this->comment; - else - $rcComment .= ': ' . $this->comment; - } - + $rcComment = $this->getRcComment(); RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '', $this->type, $this->action, $this->target, $this->comment, $this->params ); } return true; } + public function getRcComment() { + $rcComment = $this->actionText; + if( '' != $this->comment ) { + if ($rcComment == '') + $rcComment = $this->comment; + else + $rcComment .= ': ' . $this->comment; + } + return $rcComment; + } + /** * @static */ - function validTypes() { + public static function validTypes() { global $wgLogTypes; return $wgLogTypes; } @@ -98,7 +108,7 @@ class LogPage { /** * @static */ - function isLogType( $type ) { + public static function isLogType( $type ) { return in_array( $type, LogPage::validTypes() ); } @@ -120,7 +130,7 @@ class LogPage { * @todo handle missing log types * @static */ - function logHeader( $type ) { + static function logHeader( $type ) { global $wgLogHeaders; return wfMsg( $wgLogHeaders[$type] ); } @@ -128,7 +138,7 @@ class LogPage { /** * @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, $translate=false ) { global $wgLang, $wgContLang, $wgLogActions; $key = "$type/$action"; @@ -145,14 +155,17 @@ class LogPage { switch( $type ) { case 'move': $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' ); - $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), $params[0] ); + $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) ); break; case 'block': if( substr( $title->getText(), 0, 1 ) == '#' ) { $titleLink = $title->getText(); } else { - $titleLink = $skin->makeLinkObj( $title, $title->getText() ); - $titleLink .= ' (' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ), wfMsg( 'contribslink' ) ) . ')'; + // TODO: Store the user identifier in the parameters + // to make this faster for future log entries + $id = User::idFromName( $title->getText() ); + $titleLink = $skin->userLink( $id, $title->getText() ) + . $skin->userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK ); } break; case 'rights': @@ -233,7 +246,7 @@ class LogPage { * Create a blob from a parameter array * @static */ - function makeParamBlob( $params ) { + static function makeParamBlob( $params ) { return implode( "\n", $params ); } @@ -241,7 +254,7 @@ class LogPage { * Extract a parameter array from a blob * @static */ - function extractParams( $blob ) { + static function extractParams( $blob ) { if ( $blob === '' ) { return array(); } else { @@ -285,4 +298,4 @@ class LogPage { } -?> + diff --git a/includes/MacBinary.php b/includes/MacBinary.php index 2f6ad4f4..da357e52 100644 --- a/includes/MacBinary.php +++ b/includes/MacBinary.php @@ -100,7 +100,7 @@ class MacBinary { fseek( $this->handle, 0 ); $head = fread( $this->handle, 128 ); - $this->hexdump( $head ); + #$this->hexdump( $head ); if( strlen( $head ) < 128 ) { wfDebug( "$fname: couldn't read full MacBinary header\n" ); @@ -268,4 +268,4 @@ class MacBinary { } } -?> + diff --git a/includes/MagicWord.php b/includes/MagicWord.php index bf72a0c8..f7a9400d 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -86,7 +86,6 @@ class MagicWord { 'subjectpagename', 'subjectpagenamee', 'numberofusers', - 'rawsuffix', 'newsectionlink', 'numberofpages', 'currentversion', @@ -387,6 +386,181 @@ class MagicWord { function isCaseSensitive() { return $this->mCaseSensitive; } + + function getId() { + return $this->mId; + } } -?> +/** + * Class for handling an array of magic words + */ +class MagicWordArray { + var $names = array(); + var $hash; + var $baseRegex, $regex; + + function __construct( $names = array() ) { + $this->names = $names; + } + + /** + * Add a magic word by name + */ + public function add( $name ) { + global $wgContLang; + $this->names[] = $name; + $this->hash = $this->baseRegex = $this->regex = null; + } + + /** + * Add a number of magic words by name + */ + public function addArray( $names ) { + $this->names = array_merge( $this->names, array_values( $names ) ); + $this->hash = $this->baseRegex = $this->regex = null; + } + + /** + * Get a 2-d hashtable for this array + */ + function getHash() { + if ( is_null( $this->hash ) ) { + global $wgContLang; + $this->hash = array( 0 => array(), 1 => array() ); + foreach ( $this->names as $name ) { + $magic = MagicWord::get( $name ); + $case = intval( $magic->isCaseSensitive() ); + foreach ( $magic->getSynonyms() as $syn ) { + if ( !$case ) { + $syn = $wgContLang->lc( $syn ); + } + $this->hash[$case][$syn] = $name; + } + } + } + return $this->hash; + } + + /** + * Get the base regex + */ + function getBaseRegex() { + if ( is_null( $this->baseRegex ) ) { + $this->baseRegex = array( 0 => '', 1 => '' ); + foreach ( $this->names as $name ) { + $magic = MagicWord::get( $name ); + $case = intval( $magic->isCaseSensitive() ); + foreach ( $magic->getSynonyms() as $i => $syn ) { + $group = "(?P<{$i}_{$name}>" . preg_quote( $syn, '/' ) . ')'; + if ( $this->baseRegex[$case] === '' ) { + $this->baseRegex[$case] = $group; + } else { + $this->baseRegex[$case] .= '|' . $group; + } + } + } + } + return $this->baseRegex; + } + + /** + * Get an unanchored regex + */ + function getRegex() { + if ( is_null( $this->regex ) ) { + $base = $this->getBaseRegex(); + $this->regex = array( '', '' ); + if ( $this->baseRegex[0] !== '' ) { + $this->regex[0] = "/{$base[0]}/iuS"; + } + if ( $this->baseRegex[1] !== '' ) { + $this->regex[1] = "/{$base[1]}/S"; + } + } + return $this->regex; + } + + /** + * Get a regex for matching variables + */ + function getVariableRegex() { + return str_replace( "\\$1", "(.*?)", $this->getRegex() ); + } + + /** + * Get an anchored regex for matching variables + */ + function getVariableStartToEndRegex() { + $base = $this->getBaseRegex(); + $newRegex = array( '', '' ); + if ( $base[0] !== '' ) { + $newRegex[0] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[0]})$/iuS" ); + } + if ( $base[1] !== '' ) { + $newRegex[1] = str_replace( "\\$1", "(.*?)", "/^(?:{$base[1]})$/S" ); + } + return $newRegex; + } + + /** + * Parse a match array from preg_match + */ + function parseMatch( $m ) { + reset( $m ); + while ( list( $key, $value ) = each( $m ) ) { + if ( $key === 0 || $value === '' ) { + continue; + } + $parts = explode( '_', $key, 2 ); + if ( count( $parts ) != 2 ) { + // This shouldn't happen + // continue; + throw new MWException( __METHOD__ . ': bad parameter name' ); + } + list( /* $synIndex */, $magicName ) = $parts; + $paramValue = next( $m ); + return array( $magicName, $paramValue ); + } + // This shouldn't happen either + throw new MWException( __METHOD__.': parameter not found' ); + return array( false, false ); + } + + /** + * Match some text, with parameter capture + * Returns an array with the magic word name in the first element and the + * parameter in the second element. + * Both elements are false if there was no match. + */ + public function matchVariableStartToEnd( $text ) { + global $wgContLang; + $regexes = $this->getVariableStartToEndRegex(); + foreach ( $regexes as $regex ) { + if ( $regex !== '' ) { + $m = false; + if ( preg_match( $regex, $text, $m ) ) { + return $this->parseMatch( $m ); + } + } + } + return array( false, false ); + } + + /** + * Match some text, without parameter capture + * Returns the magic word name, or false if there was no capture + */ + public function matchStartToEnd( $text ) { + $hash = $this->getHash(); + if ( isset( $hash[1][$text] ) ) { + return $hash[1][$text]; + } + global $wgContLang; + $lc = $wgContLang->lc( $text ); + if ( isset( $hash[0][$lc] ) ) { + return $hash[0][$lc]; + } + return false; + } +} diff --git a/includes/Math.php b/includes/Math.php index 88934e5f..2771d04c 100644 --- a/includes/Math.php +++ b/includes/Math.php @@ -20,8 +20,9 @@ class MathRenderer { var $mathml = ''; var $conservativeness = 0; - function __construct( $tex ) { + function __construct( $tex, $params=array() ) { $this->tex = $tex; + $this->params = $params; } function setOutputMode( $mode ) { @@ -157,8 +158,8 @@ class MathRenderer { $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'math', array( 'math_inputhash' ), array( - 'math_inputhash' => $md5_sql, - 'math_outputhash' => $outmd5_sql, + 'math_inputhash' => $dbw->encodeBlob($md5_sql), + 'math_outputhash' => $dbw->encodeBlob($outmd5_sql), 'math_html_conservativeness' => $this->conservativeness, 'math_html' => $this->html, 'math_mathml' => $this->mathml, @@ -186,13 +187,13 @@ class MathRenderer { $dbr = wfGetDB( DB_SLAVE ); $rpage = $dbr->selectRow( 'math', array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ), - array( 'math_inputhash' => pack("H32", $this->md5)), # Binary packed, not hex + array( 'math_inputhash' => $dbr->encodeBlob(pack("H32", $this->md5))), # Binary packed, not hex $fname ); if( $rpage !== false ) { # Tailing 0x20s can get dropped by the database, add it back on if necessary: - $xhash = unpack( 'H32md5', $rpage->math_outputhash . " " ); + $xhash = unpack( 'H32md5', $dbr->decodeBlob($rpage->math_outputhash) . " " ); $this->hash = $xhash ['md5']; $this->conservativeness = $rpage->math_html_conservativeness; @@ -233,24 +234,44 @@ class MathRenderer { */ function _doRender() { if( $this->mode == MW_MATH_MATHML && $this->mathml != '' ) { - return "{$this->mathml}"; + return Xml::tags( 'math', + $this->_attribs( 'math', + array( 'xmlns' => 'http://www.w3.org/1998/Math/MathML' ) ), + $this->mathml ); } if (($this->mode == MW_MATH_PNG) || ($this->html == '') || (($this->mode == MW_MATH_SIMPLE) && ($this->conservativeness != 2)) || (($this->mode == MW_MATH_MODERN || $this->mode == MW_MATH_MATHML) && ($this->conservativeness == 0))) { return $this->_linkToMathImage(); } else { - return ''.$this->html.''; + return Xml::tags( 'span', + $this->_attribs( 'span', + array( 'class' => 'texhtml' ) ), + $this->html ); } } + + function _attribs( $tag, $defaults=array(), $overrides=array() ) { + $attribs = Sanitizer::validateTagAttributes( $this->params, $tag ); + $attribs = Sanitizer::mergeAttributes( $defaults, $attribs ); + $attribs = Sanitizer::mergeAttributes( $attribs, $overrides ); + return $attribs; + } function _linkToMathImage() { global $wgMathPath; - $url = htmlspecialchars( "$wgMathPath/" . substr($this->hash, 0, 1) + $url = "$wgMathPath/" . substr($this->hash, 0, 1) .'/'. substr($this->hash, 1, 1) .'/'. substr($this->hash, 2, 1) - . "/{$this->hash}.png" ); - $alt = trim(str_replace("\n", ' ', htmlspecialchars( $this->tex ))); - return "\"$alt\""; + . "/{$this->hash}.png"; + + return Xml::element( 'img', + $this->_attribs( + 'img', + array( + 'class' => 'tex', + 'alt' => $this->tex ), + array( + 'src' => $url ) ) ); } function _getHashPath() { @@ -262,11 +283,11 @@ class MathRenderer { return $path; } - public static function renderMath( $tex ) { + public static function renderMath( $tex, $params=array() ) { global $wgUser; - $math = new MathRenderer( $tex ); + $math = new MathRenderer( $tex, $params ); $math->setOutputMode( $wgUser->getOption('math')); return $math->render(); } } -?> + diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php index 60057e3a..c6cf9ac2 100644 --- a/includes/MediaTransformOutput.php +++ b/includes/MediaTransformOutput.php @@ -1,11 +1,13 @@ tag. - * May be ignored. + * + * @param array $options Associative array of options. Boolean options + * should be indicated with a value of true for true, and false or + * absent for false. + * + * alt Alternate text or caption + * desc-link Boolean, show a description link + * file-link Boolean, show a file download link + * valign vertical-align property, if the output is an inline element + * img-class Class applied to the tag, if there is such a tag + * + * For images, desc-link and file-link are implemented as a click-through. For + * sounds and videos, they may be displayed in other ways. + * + * @return string */ - abstract function toHtml( $attribs = array() , $linkAttribs = false ); + abstract function toHtml( $options = array() ); /** * This will be overridden to return true in error classes @@ -60,6 +73,19 @@ abstract class MediaTransformOutput { return $contents; } } + + function getDescLinkAttribs( $alt = false ) { + $query = $this->page ? ( 'page=' . urlencode( $this->page ) ) : ''; + $title = $this->file->getTitle(); + if ( strval( $alt ) === '' ) { + $alt = $title->getText(); + } + return array( + 'href' => $this->file->getTitle()->getLocalURL( $query ), + 'class' => 'image', + 'title' => $alt + ); + } } @@ -74,7 +100,8 @@ class ThumbnailImage extends MediaTransformOutput { * @param string $url URL path to the thumb * @private */ - function ThumbnailImage( $url, $width, $height, $path = false ) { + function ThumbnailImage( $file, $url, $width, $height, $path = false, $page = false ) { + $this->file = $file; $this->url = $url; # These should be integers when they get here. # If not, there's a bug somewhere. But let's at @@ -82,28 +109,56 @@ class ThumbnailImage extends MediaTransformOutput { $this->width = round( $width ); $this->height = round( $height ); $this->path = $path; + $this->page = $page; } /** * Return HTML tag for the thumbnail, will include * width and height attributes and a blank alt text (as required). + * + * @param array $options Associative array of options. Boolean options + * should be indicated with a value of true for true, and false or + * absent for false. * - * You can set or override additional attributes by passing an - * associative array of name => data pairs. The data will be escaped - * for HTML output, so should be in plaintext. + * alt Alternate text or caption + * desc-link Boolean, show a description link + * file-link Boolean, show a file download link + * valign vertical-align property, if the output is an inline element + * img-class Class applied to the tag, if there is such a tag * - * If $linkAttribs is given, the image will be enclosed in an tag. + * For images, desc-link and file-link are implemented as a click-through. For + * sounds and videos, they may be displayed in other ways. * - * @param array $attribs - * @param array $linkAttribs * @return string * @public */ - function toHtml( $attribs = array(), $linkAttribs = false ) { - $attribs['src'] = $this->url; - $attribs['width'] = $this->width; - $attribs['height'] = $this->height; - if( !isset( $attribs['alt'] ) ) $attribs['alt'] = ''; + function toHtml( $options = array() ) { + if ( count( func_get_args() ) == 2 ) { + throw new MWException( __METHOD__ .' called in the old style' ); + } + + $alt = empty( $options['alt'] ) ? '' : $options['alt']; + if ( !empty( $options['desc-link'] ) ) { + $linkAttribs = $this->getDescLinkAttribs( $alt ); + } elseif ( !empty( $options['file-link'] ) ) { + $linkAttribs = array( 'href' => $this->file->getURL() ); + } else { + $linkAttribs = false; + } + + $attribs = array( + 'alt' => $alt, + 'src' => $this->url, + 'width' => $this->width, + 'height' => $this->height, + 'border' => 0, + ); + if ( !empty( $options['valign'] ) ) { + $attribs['style'] = "vertical-align: {$options['valign']}"; + } + if ( !empty( $options['img-class'] ) ) { + $attribs['class'] = $options['img-class']; + } return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) ); } @@ -130,7 +185,7 @@ class MediaTransformError extends MediaTransformOutput { $this->path = false; } - function toHtml( $attribs = array(), $linkAttribs = false ) { + function toHtml( $options = array() ) { return "width}px; height: {$this->height}px;\">. + * Format a table cell. The return value should be HTML, but use an empty + * string not   for empty cells. Do not include the . + * + * The current result row is available as $this->mCurrentRow, in case you + * need more context. * * @param string $name The database field name * @param string $value The value retrieved from the database - * - * The current result row is available as $this->mCurrentRow, in case you need - * more context. */ abstract function formatValue( $name, $value ); @@ -690,10 +706,10 @@ abstract class TablePager extends IndexPager { abstract function getDefaultSort(); /** - * An array mapping database field names to a textual description of the field - * name, for use in the table header. The description should be plain text, it - * will be HTML-escaped later. + * An array mapping database field names to a textual description of the + * field name, for use in the table header. The description should be plain + * text, it will be HTML-escaped later. */ abstract function getFieldNames(); } -?> + diff --git a/includes/Parser.php b/includes/Parser.php index 8e36e170..32e7f2a8 100644 --- a/includes/Parser.php +++ b/includes/Parser.php @@ -1,5 +1,7 @@ mTagHooks = array(); + $this->mTransparentTagHooks = array(); $this->mFunctionHooks = array(); $this->mFunctionSynonyms = array( 0 => array(), 1 => array() ); $this->mFirstCall = true; } - + /** * Do various kinds of initialisation on the first call of the parser */ @@ -138,12 +142,12 @@ class Parser if ( !$this->mFirstCall ) { return; } - + 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 ); @@ -306,7 +310,7 @@ class Parser $fixtags = array( # french spaces, last one Guillemet-left # only if there is something before the space - '/(.) (?=\\?|:|;|!|\\302\\273)/' => '\\1 \\2', + '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1 \\2', # french spaces, Guillemet-right '/(\\302\\253) /' => '\\1 ', ); @@ -327,6 +331,26 @@ class Parser wfRunHooks( 'ParserBeforeTidy', array( &$this, &$text ) ); +//!JF Move to its own function + + $uniq_prefix = $this->mUniqPrefix; + $matches = array(); + $elements = array_keys( $this->mTransparentTagHooks ); + $text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix ); + + foreach( $matches as $marker => $data ) { + list( $element, $content, $params, $tag ) = $data; + $tagName = strtolower( $element ); + if( isset( $this->mTransparentTagHooks[$tagName] ) ) { + $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], + array( $content, $params, $this ) ); + } else { + $output = $tag; + } + $this->mStripState->general->setPair( $marker, $output ); + } + $text = $this->mStripState->unstripGeneral( $text ); + $text = Sanitizer::normalizeCharReferences( $text ); if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) { @@ -398,12 +422,15 @@ class Parser * Expand templates and variables in the text, producing valid, static wikitext. * Also removes comments. */ - function preprocess( $text, $title, $options ) { + function preprocess( $text, $title, $options, $revid = null ) { wfProfileIn( __METHOD__ ); $this->clearState(); $this->setOutputType( 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 ) ); @@ -449,7 +476,7 @@ class Parser * @param $text Source text string. * @param $uniq_prefix * - * @private + * @public * @static */ function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){ @@ -480,7 +507,7 @@ class Parser $inside = $p[4]; } - $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . '-QINU'; + $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . "-QINU\x07"; $stripped .= $marker; if ( $close === '/>' ) { @@ -587,7 +614,8 @@ class Parser $output = Xml::escapeTagsOnly( $content ); break; case 'math': - $output = $wgContLang->armourMath( MathRenderer::renderMath( $content ) ); + $output = $wgContLang->armourMath( + MathRenderer::renderMath( $content, $params ) ); break; case 'gallery': $output = $this->renderImageGallery( $content, $params ); @@ -725,7 +753,7 @@ class Parser $descriptorspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('file', '/dev/null', 'a') // FIXME: this line in UNIX-specific, it generates a warning on Windows, because /dev/null is not a valid Windows file. + 2 => array('file', wfGetNull(), 'a') ); $pipes = array(); $process = proc_open("$wgTidyBin -config $wgTidyConf $wgTidyOpts$opts", $descriptorspec, $pipes); @@ -1000,7 +1028,7 @@ class Parser $text = strtr( $text, array( '' => '', '' => '') ); $text = StringUtils::delimiterReplace( '', '', '', $text ); - $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ) ); + $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), array(), array_keys( $this->mTransparentTagHooks ) ); $text = $this->replaceVariables( $text, $args ); wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) ); @@ -1797,11 +1825,15 @@ class Parser $this->mOutput->addImage( $nt->getDBkey() ); continue; } elseif( $ns == NS_SPECIAL ) { - $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix ); + 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 = new Image( $nt ); - if( $img->exists() ) { + $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. @@ -1918,15 +1950,22 @@ class Parser wfProfileIn( $fname ); $ret = $target; # default return value is no change - # bug 7425 - $target = trim( $target ); - # 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]) ); @@ -1934,9 +1973,9 @@ class Parser $noslash = substr( $target, 1 ); } - $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash); + $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix; if( '' === $text ) { - $text = $target; + $text = $target . $suffix; } # this might be changed for ugliness reasons } else { # check for .. subpage backlinks @@ -1954,13 +1993,14 @@ class Parser if( substr( $nodotdot, -1, 1 ) == '/' ) { $nodotdot = substr( $nodotdot, 0, -1 ); if( '' === $text ) { - $text = $nodotdot; + $text = $nodotdot . $suffix; } } $nodotdot = trim( $nodotdot ); if( $nodotdot != '' ) { $ret .= '/' . $nodotdot; } + $ret .= $suffix; } } } @@ -2406,6 +2446,8 @@ class Parser $oldtz = getenv( 'TZ' ); putenv( 'TZ='.$wgLocaltimezone ); } + + wfSuppressWarnings(); // E_STRICT system time bitching $localTimestamp = date( 'YmdHis', $ts ); $localMonth = date( 'm', $ts ); $localMonthName = date( 'n', $ts ); @@ -2418,20 +2460,21 @@ class Parser if ( isset( $wgLocaltimezone ) ) { putenv( 'TZ='.$oldtz ); } + wfRestoreWarnings(); switch ( $index ) { case 'currentmonth': - return $varCache[$index] = $wgContLang->formatNum( date( 'm', $ts ) ); + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) ); case 'currentmonthname': - return $varCache[$index] = $wgContLang->getMonthName( date( 'n', $ts ) ); + return $varCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) ); case 'currentmonthnamegen': - return $varCache[$index] = $wgContLang->getMonthNameGen( date( 'n', $ts ) ); + return $varCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) ); case 'currentmonthabbrev': - return $varCache[$index] = $wgContLang->getMonthAbbreviation( date( 'n', $ts ) ); + return $varCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) ); case 'currentday': - return $varCache[$index] = $wgContLang->formatNum( date( 'j', $ts ) ); + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) ); case 'currentday2': - return $varCache[$index] = $wgContLang->formatNum( date( 'd', $ts ) ); + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) ); case 'localmonth': return $varCache[$index] = $wgContLang->formatNum( $localMonth ); case 'localmonthname': @@ -2445,25 +2488,25 @@ class Parser case 'localday2': return $varCache[$index] = $wgContLang->formatNum( $localDay2 ); case 'pagename': - return $this->mTitle->getText(); + return wfEscapeWikiText( $this->mTitle->getText() ); case 'pagenamee': return $this->mTitle->getPartialURL(); case 'fullpagename': - return $this->mTitle->getPrefixedText(); + return wfEscapeWikiText( $this->mTitle->getPrefixedText() ); case 'fullpagenamee': return $this->mTitle->getPrefixedURL(); case 'subpagename': - return $this->mTitle->getSubpageText(); + return wfEscapeWikiText( $this->mTitle->getSubpageText() ); case 'subpagenamee': return $this->mTitle->getSubpageUrlForm(); case 'basepagename': - return $this->mTitle->getBaseText(); + 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 $talkPage->getPrefixedText(); + return wfEscapeWikiText( $talkPage->getPrefixedText() ); } else { return ''; } @@ -2476,7 +2519,7 @@ class Parser } case 'subjectpagename': $subjPage = $this->mTitle->getSubjectPage(); - return $subjPage->getPrefixedText(); + return wfEscapeWikiText( $subjPage->getPrefixedText() ); case 'subjectpagenamee': $subjPage = $this->mTitle->getSubjectPage(); return $subjPage->getPrefixedUrl(); @@ -2505,19 +2548,19 @@ class Parser case 'subjectspacee': return( wfUrlencode( $this->mTitle->getSubjectNsText() ) ); case 'currentdayname': - return $varCache[$index] = $wgContLang->getWeekdayName( date( 'w', $ts ) + 1 ); + return $varCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 ); case 'currentyear': - return $varCache[$index] = $wgContLang->formatNum( date( 'Y', $ts ), true ); + 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( date( 'H', $ts ), true ); + 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)date( 'W', $ts ) ); + return $varCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) ); case 'currentdow': - return $varCache[$index] = $wgContLang->formatNum( date( 'w', $ts ) ); + return $varCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) ); case 'localdayname': return $varCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 ); case 'localyear': @@ -3089,7 +3132,7 @@ class Parser $found = false; //access denied wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() ); } else { - $articleContent = $this->fetchTemplate( $title ); + list($articleContent,$title) = $this->fetchTemplateAndtitle( $title ); if ( $articleContent !== false ) { $found = true; $text = $articleContent; @@ -3220,6 +3263,7 @@ class Parser 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; @@ -3255,13 +3299,26 @@ class Parser /** * Fetch the unparsed text of a template and register a reference to it. */ - function fetchTemplate( $title ) { - $text = false; + function fetchTemplateAndtitle( $title ) { + $text = $skip = false; + $finalTitle = $title; // Loop to fetch the article, with up to 1 redirect for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) { - $rev = Revision::newFromTitle( $title ); - $this->mOutput->addTemplate( $title, $title->getArticleID() ); - if ( $rev ) { + # Give extensions a chance to select the revision instead + $id = false; // Assume current + wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( &$this, &$title, &$skip, &$id ) ); + + if( $skip ) { + $text = false; + $this->mOutput->addTemplate( $title, $title->getArticleID(), null ); + break; + } + $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title ); + $rev_id = $rev ? $rev->getId() : 0; + + $this->mOutput->addTemplate( $title, $title->getArticleID(), $rev_id ); + + if( $rev ) { $text = $rev->getText(); } elseif( $title->getNamespace() == NS_MEDIAWIKI ) { global $wgLang; @@ -3278,9 +3335,15 @@ class Parser break; } // Redirect? + $finalTitle = $title; $title = Title::newFromRedirect( $text ); } - return $text; + return array($text,$finalTitle); + } + + function fetchTemplate( $title ) { + $rv = $this->fetchTemplateAndtitle($title); + return $rv[0]; } /** @@ -3375,7 +3438,13 @@ class Parser } /** - * Detect __TOC__ magic word and set a placeholder + * 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, @@ -3453,17 +3522,13 @@ class Parser $enoughToc = true; } - # Never ever show TOC if no headers - if( $numMatches < 1 ) { - $enoughToc = false; - } - # 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 @@ -3504,7 +3569,9 @@ class Parser $toclevel++; $sublevelCount[$toclevel] = 0; if( $toclevel<$wgMaxTocLevel ) { + $prevtoclevel = $toclevel; $toc .= $sk->tocIndent(); + $numVisible++; } } elseif ( $level < $prevlevel && $toclevel > 1 ) { @@ -3528,7 +3595,12 @@ class Parser } } if( $toclevel<$wgMaxTocLevel ) { - $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel ); + if($prevtoclevel < $wgMaxTocLevel) { + # Unindent only if the previous toc level was shown :p + $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel ); + } else { + $toc .= $sk->tocLineEnd(); + } } } else { @@ -3569,12 +3641,21 @@ class Parser "\$this->mInterwikiLinkHolders['texts'][\$1]", $canonized_headline ); - # strip out HTML - $canonized_headline = preg_replace( '/<.*?' . '>/','',$canonized_headline ); - $tocline = trim( $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 = trim( $canonized_headline ); - $canonized_headline = Sanitizer::escapeId( $tocline ); + $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 @@ -3611,9 +3692,14 @@ class Parser $sectionCount++; } + # Never ever show TOC if no headers + if( $numVisible < 1 ) { + $enoughToc = false; + } + if( $enoughToc ) { - if( $toclevel<$wgMaxTocLevel ) { - $toc .= $sk->tocUnindent( $toclevel - 1 ); + if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) { + $toc .= $sk->tocUnindent( $prevtoclevel - 1 ); } $toc = $sk->tocList( $toc ); } @@ -3759,11 +3845,16 @@ class Parser * @private */ function getUserSig( &$user ) { + global $wgMaxSigChars; + $username = $user->getName(); $nickname = $user->getOption( 'nickname' ); $nickname = $nickname === '' ? $username : $nickname; - - if( $user->getBoolOption( 'fancysig' ) !== false ) { + + 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 @@ -3903,6 +3994,14 @@ class Parser 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: @@ -4018,6 +4117,8 @@ class Parser $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 ) ) { @@ -4055,16 +4156,11 @@ class Parser $linkCache->addGoodLinkObj( $s->page_id, $title ); $this->mOutput->addLink( $title, $s->page_id ); - if ( $threshold > 0 ) { - $size = $s->page_len; - if ( $s->page_is_redirect || $s->page_namespace != 0 || $size >= $threshold ) { - $colours[$pdbk] = 1; - } else { - $colours[$pdbk] = 2; - } - } else { - $colours[$pdbk] = 1; - } + $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' ); @@ -4104,7 +4200,7 @@ class Parser } // process categories, check if a category exists in some variant - foreach( $categories as $category){ + foreach( $categories as $category ){ $variants = $wgContLang->convertLinkToAllVariants($category); foreach($variants as $variant){ if($variant != $category){ @@ -4324,8 +4420,11 @@ class Parser $ig->setContextTitle( $this->mTitle ); $ig->setShowBytes( false ); $ig->setShowFilename( false ); - $ig->setParsing(); + $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']; @@ -4342,6 +4441,8 @@ class Parser if( isset( $params['heights'] ) ) { $ig->setHeights( $params['heights'] ); } + + wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) ); $lines = explode( "\n", $text ); foreach ( $lines as $line ) { @@ -4373,7 +4474,7 @@ class Parser ); $html = $pout->getText(); - $ig->add( new Image( $nt ), $html ); + $ig->add( $nt, $html ); # Only add real images (bug #5586) if ( $nt->getNamespace() == NS_IMAGE ) { @@ -4383,10 +4484,50 @@ class Parser 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( $nt, $options ) { + function makeImage( $title, $options ) { # @TODO: let the MediaHandler specify its transform parameters # # Check if the options text is of the form "options|alt text" @@ -4398,6 +4539,9 @@ class Parser # * ___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 @@ -4407,67 +4551,66 @@ class Parser # * 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 ) ); - $part = array_map( 'trim', explode( '|', $options) ); - - $mwAlign = array(); - $alignments = array( 'left', 'right', 'center', 'none', 'baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom' ); - foreach ( $alignments as $alignment ) { - $mwAlign[$alignment] =& MagicWord::get( 'img_'.$alignment ); + if ( $skip ) { + return $sk->makeLinkObj( $title ); } - $mwThumb =& MagicWord::get( 'img_thumbnail' ); - $mwManualThumb =& MagicWord::get( 'img_manualthumb' ); - $mwWidth =& MagicWord::get( 'img_width' ); - $mwFramed =& MagicWord::get( 'img_framed' ); - $mwPage =& MagicWord::get( 'img_page' ); - $caption = ''; - $params = array(); - $framed = $thumb = false; - $manual_thumb = '' ; - $align = $valign = ''; - $sk = $this->mOptions->getSkin(); + # Get parameter map + $file = wfFindFile( $title, $time ); + $handler = $file ? $file->getHandler() : false; - foreach( $part as $val ) { - if ( !is_null( $mwThumb->matchVariableStartToEnd($val) ) ) { - $thumb=true; - } elseif ( ! is_null( $match = $mwManualThumb->matchVariableStartToEnd($val) ) ) { - # use manually specified thumbnail - $thumb=true; - $manual_thumb = $match; - } else { - foreach( $alignments as $alignment ) { - if ( ! is_null( $mwAlign[$alignment]->matchVariableStartToEnd($val) ) ) { - switch ( $alignment ) { - case 'left': case 'right': case 'center': case 'none': - $align = $alignment; break; - default: - $valign = $alignment; - } - continue 2; - } - } - if ( ! is_null( $match = $mwPage->matchVariableStartToEnd($val) ) ) { - # Select a page in a multipage document - $params['page'] = $match; - } elseif ( !isset( $params['width'] ) && ! is_null( $match = $mwWidth->matchVariableStartToEnd($val) ) ) { - wfDebug( "img_width match: $match\n" ); - # $match is the image width in pixels + 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]*)$/', $match, $m ) ) { - $params['width'] = intval( $m[1] ); - $params['height'] = intval( $m[2] ); + if ( preg_match( '/^([0-9]*)x([0-9]*)$/', $value, $m ) ) { + $params[$type]['width'] = intval( $m[1] ); + $params[$type]['height'] = intval( $m[2] ); } else { - $params['width'] = intval($match); + $params[$type]['width'] = intval( $value ); } - } elseif ( ! is_null( $mwFramed->matchVariableStartToEnd($val) ) ) { - $framed=true; - } else { - $caption = $val; + } + } 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 ); @@ -4477,8 +4620,18 @@ class Parser $alt = $this->mStripState->unstripBoth( $alt ); $alt = Sanitizer::stripAllTags( $alt ); + $params['frame']['alt'] = $alt; + $params['frame']['caption'] = $caption; + # Linker does the rest - return $sk->makeImageLinkObj( $nt, $caption, $alt, $align, $params, $framed, $thumb, $manual_thumb, $valign ); + $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; } /** @@ -4517,7 +4670,7 @@ class Parser /**#@+ * Accessor */ - function getTags() { return array_keys( $this->mTagHooks ); } + function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); } /**#@-*/ @@ -4537,6 +4690,10 @@ class Parser * 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; @@ -4554,7 +4711,7 @@ class Parser # 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)"; + $comment = "(?:$uniq-!--.*?QINU\x07)"; $secs = preg_split( "/ ( @@ -4717,7 +4874,6 @@ class Parser : $this->mTitle->getPrefixedText(); } } - } /** @@ -4770,5 +4926,3 @@ class StripState { return $text; } } - -?> diff --git a/includes/ParserCache.php b/includes/ParserCache.php index 1489fcf9..129b7132 100644 --- a/includes/ParserCache.php +++ b/includes/ParserCache.php @@ -116,4 +116,4 @@ class ParserCache { } -?> + diff --git a/includes/ParserOptions.php b/includes/ParserOptions.php index e335720f..2200bfea 100644 --- a/includes/ParserOptions.php +++ b/includes/ParserOptions.php @@ -116,4 +116,4 @@ class ParserOptions } } -?> + diff --git a/includes/ParserOutput.php b/includes/ParserOutput.php index 03f1819c..d4daf1d1 100644 --- a/includes/ParserOutput.php +++ b/includes/ParserOutput.php @@ -14,13 +14,18 @@ class ParserOutput $mTitleText, # title text of the chosen language variant $mLinks, # 2-D map of NS/DBK to ID for the links in the document. ID=zero for broken. $mTemplates, # 2-D map of NS/DBK to ID for the template references. ID=zero for broken. + $mTemplateIds, # 2-D map of NS/DBK to rev ID for the template references. ID=zero for broken. $mImages, # DB keys of the images used, in the array key only $mExternalLinks, # External link URLs, in the key only - $mHTMLtitle, # Display HTML title - $mSubtitle, # Additional subtitle $mNewSection, # Show a new section link? $mNoGallery, # No gallery on category page? (__NOGALLERY__) - $mHeadItems; # Items to put in the section + $mHeadItems, # Items to put in the section + $mOutputHooks; # Hook tags as per $wgParserOutputHooks + + /** + * Overridden title for display + */ + private $displayTitle = false; function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(), $containsOldMagic = false, $titletext = '' ) @@ -36,15 +41,15 @@ class ParserOutput $this->mTemplates = array(); $this->mImages = array(); $this->mExternalLinks = array(); - $this->mHTMLtitle = "" ; - $this->mSubtitle = "" ; $this->mNewSection = false; $this->mNoGallery = false; $this->mHeadItems = array(); + $this->mTemplateIds = array(); + $this->mOutputHooks = array(); } function getText() { return $this->mText; } - function &getLanguageLinks() { return $this->mLanguageLinks; } + function &getLanguageLinks() { return $this->mLanguageLinks; } function getCategoryLinks() { return array_keys( $this->mCategories ); } function &getCategories() { return $this->mCategories; } function getCacheTime() { return $this->mCacheTime; } @@ -55,6 +60,7 @@ class ParserOutput function &getExternalLinks() { return $this->mExternalLinks; } function getNoGallery() { return $this->mNoGallery; } function getSubtitle() { return $this->mSubtitle; } + function getOutputHooks() { return (array)$this->mOutputHooks; } function containsOldMagic() { return $this->mContainsOldMagic; } function setText( $text ) { return wfSetVar( $this->mText, $text ); } @@ -63,13 +69,15 @@ class ParserOutput 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 setSubtitle( $st ) { return wfSetVar( $this->mSubtitle, $st ); } function addCategory( $c, $sort ) { $this->mCategories[$c] = $sort; } - function addImage( $name ) { $this->mImages[$name] = 1; } function addLanguageLink( $t ) { $this->mLanguageLinks[] = $t; } function addExternalLink( $url ) { $this->mExternalLinks[$url] = 1; } + function addOutputHook( $hook, $data = false ) { + $this->mOutputHooks[] = array( $hook, $data ); + } + function setNewSection( $value ) { $this->mNewSection = (bool)$value; } @@ -88,14 +96,22 @@ class ParserOutput } $this->mLinks[$ns][$dbk] = $id; } + + function addImage( $name ) { + $this->mImages[$name] = 1; + } - function addTemplate( $title, $id ) { + function addTemplate( $title, $page_id, $rev_id ) { $ns = $title->getNamespace(); $dbk = $title->getDBkey(); if ( !isset( $this->mTemplates[$ns] ) ) { $this->mTemplates[$ns] = array(); } - $this->mTemplates[$ns][$dbk] = $id; + $this->mTemplates[$ns][$dbk] = $page_id; + if ( !isset( $this->mTemplateIds[$ns] ) ) { + $this->mTemplateIds[$ns] = array(); + } + $this->mTemplateIds[$ns][$dbk] = $rev_id; // For versioning } /** @@ -128,6 +144,27 @@ class ParserOutput $this->mHeadItems[] = $section; } } + + /** + * Override the title to be used for display + * -- this is assumed to have been validated + * (check equal normalisation, etc.) + * + * @param string $text Desired title text + */ + public function setDisplayTitle( $text ) { + $this->displayTitle = $text; + } + + /** + * Get the title to be used for display + * + * @return string + */ + public function getDisplayTitle() { + return $this->displayTitle; + } + } -?> + diff --git a/includes/PatrolLog.php b/includes/PatrolLog.php index a22839ff..35cb4a02 100644 --- a/includes/PatrolLog.php +++ b/includes/PatrolLog.php @@ -46,14 +46,19 @@ class PatrolLog { # these conditions would have gone into recentchanges, which we aren't # supposed to be updating if( is_object( $skin ) ) { - list( $cur, $prev, $auto ) = $params; + list( $cur, /* $prev */, $auto ) = $params; # Standard link to the page in question $link = $skin->makeLinkObj( $title ); - # Generate a diff link - $bits[] = 'oldid=' . urlencode( $cur ); - $bits[] = 'diff=prev'; - $bits = implode( '&', $bits ); - $diff = $skin->makeLinkObj( $title, htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ), $bits ); + if( $title->exists() ) { + # Generate a diff link + $bits[] = 'oldid=' . urlencode( $cur ); + $bits[] = 'diff=prev'; + $bits = implode( '&', $bits ); + $diff = $skin->makeKnownLinkObj( $title, htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ), $bits ); + } else { + # Don't bother with a diff link, it's useless + $diff = htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ); + } # Indicate whether or not the patrolling was automatic $auto = $auto ? wfMsgHtml( 'patrol-log-auto' ) : ''; # Put it all together @@ -80,4 +85,3 @@ class PatrolLog { } -?> \ No newline at end of file diff --git a/includes/Profiler.php b/includes/Profiler.php index da3a82ed..8e1cd147 100644 --- a/includes/Profiler.php +++ b/includes/Profiler.php @@ -301,6 +301,9 @@ class Profiler { * @static */ function logToDB($name, $timeSum, $eventCount) { + # Do not log anything if database is readonly (bug 5375) + if( wfReadOnly() ) { return; } + # Warning: $wguname is a live patch, it should be moved to Setup.php global $wguname, $wgProfilePerHost; @@ -361,4 +364,4 @@ class Profiler { } -?> + diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php index f43c7dfc..b07f2517 100644 --- a/includes/ProfilerSimple.php +++ b/includes/ProfilerSimple.php @@ -122,4 +122,4 @@ class ProfilerSimple extends Profiler { } } } -?> + diff --git a/includes/ProfilerSimpleUDP.php b/includes/ProfilerSimpleUDP.php index 500f1cbd..7d2f7e21 100644 --- a/includes/ProfilerSimpleUDP.php +++ b/includes/ProfilerSimpleUDP.php @@ -37,4 +37,4 @@ class ProfilerSimpleUDP extends ProfilerSimple { socket_sendto($sock,$packet,$plength,0x100,$wgUDPProfilerHost,$wgUDPProfilerPort); } } -?> + diff --git a/includes/ProfilerStub.php b/includes/ProfilerStub.php index 4cf0aa44..c41845a4 100644 --- a/includes/ProfilerStub.php +++ b/includes/ProfilerStub.php @@ -23,4 +23,4 @@ function wfGetProfilingOutput( $s, $e ) {} function wfProfileClose() {} $wgProfiling = false; -?> + diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 3cafbd55..c249ec12 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -76,11 +76,12 @@ class ProtectionForm { } function execute() { - global $wgRequest; + global $wgRequest, $wgOut; if( $wgRequest->wasPosted() ) { if( $this->save() ) { - global $wgOut; - $wgOut->redirect( $this->mTitle->getFullUrl() ); + $article = new Article( $this->mTitle ); + $q = $article->isRedirect() ? 'redirect=no' : ''; + $wgOut->redirect( $this->mTitle->getFullUrl( $q ) ); } } else { $this->show(); @@ -99,7 +100,7 @@ class ProtectionForm { return; } - list( $cascadeSources, $restrictions ) = $this->mTitle->getCascadeProtectionSources(); + list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources(); if ( "" != $err ) { $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) ); @@ -188,6 +189,13 @@ class ProtectionForm { if( !$ok ) { throw new FatalError( "Unknown error at restriction save time." ); } + + if( $wgRequest->getCheck( 'mwProtectWatch' ) ) { + $this->mArticle->doWatch(); + } elseif( $this->mTitle->userIsWatching() ) { + $this->mArticle->doUnwatch(); + } + return $ok; } @@ -232,18 +240,18 @@ class ProtectionForm { $out .= "\n"; $out .= "
    " . $this->htmlMsg . @@ -158,9 +213,10 @@ class MediaTransformError extends MediaTransformOutput { class TransformParameterError extends MediaTransformError { function __construct( $params ) { parent::__construct( 'thumbnail_error', - max( @$params['width'], 180 ), max( @$params['height'], 180 ), + max( isset( $params['width'] ) ? $params['width'] : 0, 180 ), + max( isset( $params['height'] ) ? $params['height'] : 0, 180 ), wfMsg( 'thumbnail_invalid_params' ) ); } } -?> + diff --git a/includes/MemcachedSessions.php b/includes/MemcachedSessions.php index 3bcf5535..3b248cf0 100644 --- a/includes/MemcachedSessions.php +++ b/includes/MemcachedSessions.php @@ -69,4 +69,4 @@ function memsess_gc( $maxlifetime ) { session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', 'memsess_write', 'memsess_destroy', 'memsess_gc' ); -?> + diff --git a/includes/MessageCache.php b/includes/MessageCache.php index e2cbf5f6..10c95a7e 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -23,6 +23,7 @@ class MessageCache { var $mExtensionMessages = array(); var $mInitialised = false; var $mDeferred = true; + var $mAllMessagesLoaded; function __construct( &$memCached, $useDB, $expiry, $memcPrefix) { wfProfileIn( __METHOD__ ); @@ -627,7 +628,6 @@ class MessageCache { /** * Add a 2-D array of messages by lang. Useful for extensions. - * Introduced in 1.9. Please do not use it for now, for backwards compatibility. * * @param array $messages The array to be added */ @@ -670,12 +670,39 @@ class MessageCache { } } - static function loadAllMessages() { + function loadAllMessages() { + global $wgExtensionMessagesFiles; + if ( $this->mAllMessagesLoaded ) { + return; + } + $this->mAllMessagesLoaded = true; + # Some extensions will load their messages when you load their class file wfLoadAllExtensions(); # Others will respond to this hook wfRunHooks( 'LoadAllMessages' ); + # Some register their messages in $wgExtensionMessagesFiles + foreach ( $wgExtensionMessagesFiles as $name => $file ) { + if ( $file ) { + $this->loadMessagesFile( $file ); + $wgExtensionMessagesFiles[$name] = false; + } + } # Still others will respond to neither, they are EVIL. We sometimes need to know! } + + /** + * Load messages from a given file + */ + function loadMessagesFile( $filename ) { + $magicWords = false; + require( $filename ); + $this->addMessagesByLang( $messages ); + + if ( $magicWords !== false ) { + global $wgContLang; + $wgContLang->addMagicWordsByLang( $magicWords ); + } + } } -?> + diff --git a/includes/Metadata.php b/includes/Metadata.php index b995b223..f5b0b247 100644 --- a/includes/Metadata.php +++ b/includes/Metadata.php @@ -365,4 +365,4 @@ function getKnownLicenses() { return $knownLicenses; } -?> + diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index db35535d..264a3595 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -111,56 +111,67 @@ class MimeMagic { * --- load mime.types --- */ - global $wgMimeTypeFile; + global $wgMimeTypeFile, $IP; - $types= MM_WELL_KNOWN_MIME_TYPES; + $types = MM_WELL_KNOWN_MIME_TYPES; - if ($wgMimeTypeFile) { - if (is_file($wgMimeTypeFile) and is_readable($wgMimeTypeFile)) { - wfDebug("MimeMagic::MimeMagic: loading mime types from $wgMimeTypeFile\n"); - - $types.= "\n"; - $types.= file_get_contents($wgMimeTypeFile); + if ( $wgMimeTypeFile == 'includes/mime.types' ) { + $wgMimeTypeFile = "$IP/$wgMimeTypeFile"; + } + + if ( $wgMimeTypeFile ) { + if ( is_file( $wgMimeTypeFile ) and is_readable( $wgMimeTypeFile ) ) { + wfDebug( __METHOD__.": loading mime types from $wgMimeTypeFile\n" ); + $types .= "\n"; + $types .= file_get_contents( $wgMimeTypeFile ); + } else { + wfDebug( __METHOD__.": can't load mime types from $wgMimeTypeFile\n" ); } - else wfDebug("MimeMagic::MimeMagic: can't load mime types from $wgMimeTypeFile\n"); + } else { + wfDebug( __METHOD__.": no mime types file defined, using build-ins only.\n" ); } - else wfDebug("MimeMagic::MimeMagic: no mime types file defined, using build-ins only.\n"); - $types= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$types); - $types= str_replace("\t"," ",$types); + $types = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $types ); + $types = str_replace( "\t", " ", $types ); - $this->mMimeToExt= array(); - $this->mToMime= array(); + $this->mMimeToExt = array(); + $this->mToMime = array(); - $lines= explode("\n",$types); - foreach ($lines as $s) { - $s= trim($s); - if (empty($s)) continue; - if (strpos($s,'#')===0) continue; + $lines = explode( "\n",$types ); + foreach ( $lines as $s ) { + $s = trim( $s ); + if ( empty( $s ) ) continue; + if ( strpos( $s, '#' ) === 0 ) continue; - $s= strtolower($s); - $i= strpos($s,' '); + $s = strtolower( $s ); + $i = strpos( $s, ' ' ); - if ($i===false) continue; + if ( $i === false ) continue; #print "processing MIME line $s
    "; - $mime= substr($s,0,$i); - $ext= trim(substr($s,$i+1)); + $mime = substr( $s, 0, $i ); + $ext = trim( substr($s, $i+1 ) ); - if (empty($ext)) continue; + if ( empty( $ext ) ) continue; - if ( !empty($this->mMimeToExt[$mime])) $this->mMimeToExt[$mime] .= ' '.$ext; - else $this->mMimeToExt[$mime]= $ext; + if ( !empty( $this->mMimeToExt[$mime] ) ) { + $this->mMimeToExt[$mime] .= ' ' . $ext; + } else { + $this->mMimeToExt[$mime] = $ext; + } - $extensions= explode(' ',$ext); + $extensions = explode( ' ', $ext ); - foreach ($extensions as $e) { - $e= trim($e); - if (empty($e)) continue; + foreach ( $extensions as $e ) { + $e = trim( $e ); + if ( empty( $e ) ) continue; - if ( !empty($this->mExtToMime[$e])) $this->mExtToMime[$e] .= ' '.$mime; - else $this->mExtToMime[$e]= $mime; + if ( !empty( $this->mExtToMime[$e] ) ) { + $this->mExtToMime[$e] .= ' ' . $mime; + } else { + $this->mExtToMime[$e] = $mime; + } } } @@ -169,62 +180,69 @@ class MimeMagic { */ global $wgMimeInfoFile; + if ( $wgMimeInfoFile == 'includes/mime.info' ) { + $wgMimeInfoFile = "$IP/$wgMimeInfoFile"; + } - $info= MM_WELL_KNOWN_MIME_INFO; - - if ($wgMimeInfoFile) { - if (is_file($wgMimeInfoFile) and is_readable($wgMimeInfoFile)) { - wfDebug("MimeMagic::MimeMagic: loading mime info from $wgMimeInfoFile\n"); + $info = MM_WELL_KNOWN_MIME_INFO; - $info.= "\n"; - $info.= file_get_contents($wgMimeInfoFile); + if ( $wgMimeInfoFile ) { + if ( is_file( $wgMimeInfoFile ) and is_readable( $wgMimeInfoFile ) ) { + wfDebug( __METHOD__.": loading mime info from $wgMimeInfoFile\n" ); + $info .= "\n"; + $info .= file_get_contents( $wgMimeInfoFile ); + } else { + wfDebug(__METHOD__.": can't load mime info from $wgMimeInfoFile\n"); } - else wfDebug("MimeMagic::MimeMagic: can't load mime info from $wgMimeInfoFile\n"); + } else { + wfDebug(__METHOD__.": no mime info file defined, using build-ins only.\n"); } - else wfDebug("MimeMagic::MimeMagic: no mime info file defined, using build-ins only.\n"); - $info= str_replace(array("\r\n","\n\r","\n\n","\r\r","\r"),"\n",$info); - $info= str_replace("\t"," ",$info); + $info = str_replace( array( "\r\n", "\n\r", "\n\n", "\r\r", "\r" ), "\n", $info); + $info = str_replace( "\t", " ", $info ); - $this->mMimeTypeAliases= array(); - $this->mMediaTypes= array(); + $this->mMimeTypeAliases = array(); + $this->mMediaTypes = array(); - $lines= explode("\n",$info); - foreach ($lines as $s) { - $s= trim($s); - if (empty($s)) continue; - if (strpos($s,'#')===0) continue; + $lines = explode( "\n", $info ); + foreach ( $lines as $s ) { + $s = trim( $s ); + if ( empty( $s ) ) continue; + if ( strpos( $s, '#' ) === 0 ) continue; - $s= strtolower($s); - $i= strpos($s,' '); + $s = strtolower( $s ); + $i = strpos( $s, ' ' ); - if ($i===false) continue; + if ( $i === false ) continue; #print "processing MIME INFO line $s
    "; - $match= array(); - if (preg_match('!\[\s*(\w+)\s*\]!',$s,$match)) { - $s= preg_replace('!\[\s*(\w+)\s*\]!','',$s); - $mtype= trim(strtoupper($match[1])); + $match = array(); + if ( preg_match( '!\[\s*(\w+)\s*\]!', $s, $match ) ) { + $s = preg_replace( '!\[\s*(\w+)\s*\]!', '', $s ); + $mtype = trim( strtoupper( $match[1] ) ); + } else { + $mtype = MEDIATYPE_UNKNOWN; } - else $mtype= MEDIATYPE_UNKNOWN; - $m= explode(' ',$s); + $m = explode( ' ', $s ); - if (!isset($this->mMediaTypes[$mtype])) $this->mMediaTypes[$mtype]= array(); + if ( !isset( $this->mMediaTypes[$mtype] ) ) { + $this->mMediaTypes[$mtype] = array(); + } - foreach ($m as $mime) { - $mime= trim($mime); - if (empty($mime)) continue; + foreach ( $m as $mime ) { + $mime = trim( $mime ); + if ( empty( $mime ) ) continue; - $this->mMediaTypes[$mtype][]= $mime; + $this->mMediaTypes[$mtype][] = $mime; } - if (sizeof($m)>1) { - $main= $m[0]; - for ($i=1; $imMimeTypeAliases[$mime]= $main; + if ( sizeof( $m ) > 1 ) { + $main = $m[0]; + for ( $i=1; $imMimeTypeAliases[$mime] = $main; } } } @@ -244,14 +262,14 @@ class MimeMagic { /** returns a list of file extensions for a given mime type * as a space separated string. */ - function getExtensionsForType($mime) { - $mime= strtolower($mime); + function getExtensionsForType( $mime ) { + $mime = strtolower( $mime ); - $r= @$this->mMimeToExt[$mime]; + $r = @$this->mMimeToExt[$mime]; - if (@!$r and isset($this->mMimeTypeAliases[$mime])) { - $mime= $this->mMimeTypeAliases[$mime]; - $r= @$this->mMimeToExt[$mime]; + if ( @!$r and isset( $this->mMimeTypeAliases[$mime] ) ) { + $mime = $this->mMimeTypeAliases[$mime]; + $r = @$this->mMimeToExt[$mime]; } return $r; @@ -260,22 +278,22 @@ class MimeMagic { /** returns a list of mime types for a given file extension * as a space separated string. */ - function getTypesForExtension($ext) { - $ext= strtolower($ext); + function getTypesForExtension( $ext ) { + $ext = strtolower( $ext ); - $r= isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null; + $r = isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null; return $r; } /** returns a single mime type for a given file extension. * This is always the first type from the list returned by getTypesForExtension($ext). */ - function guessTypesForExtension($ext) { - $m= $this->getTypesForExtension( $ext ); - if( is_null($m) ) return NULL; + function guessTypesForExtension( $ext ) { + $m = $this->getTypesForExtension( $ext ); + if ( is_null( $m ) ) return NULL; - $m= trim( $m ); - $m= preg_replace('/\s.*$/','',$m); + $m = trim( $m ); + $m = preg_replace( '/\s.*$/', '', $m ); return $m; } @@ -285,17 +303,17 @@ class MimeMagic { * returns true if a match was found, NULL if the mime type is unknown, * and false if the mime type is known but no matches where found. */ - function isMatchingExtension($extension,$mime) { - $ext= $this->getExtensionsForType($mime); + function isMatchingExtension( $extension, $mime ) { + $ext = $this->getExtensionsForType( $mime ); - if (!$ext) { + if ( !$ext ) { return NULL; //unknown } - $ext= explode(' ',$ext); + $ext = explode( ' ', $ext ); - $extension= strtolower($extension); - if (in_array($extension,$ext)) { + $extension = strtolower( $extension ); + if ( in_array( $extension, $ext ) ) { return true; } @@ -347,16 +365,18 @@ class MimeMagic { * or misinterpreter by the default mime detection (namely xml based formats like XHTML or SVG). * * @param string $file The file to check - * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * @param mixed $ext The file extension, or true to extract it from the filename. + * Set it to false to ignore the extension. * * @return string the mime type of $file */ - function guessMimeType( $file, $useExt=true ) { - $fname = 'MimeMagic::guessMimeType'; - $mime= $this->detectMimeType($file,$useExt); + function guessMimeType( $file, $ext = true ) { + $mime = $this->detectMimeType( $file, $ext ); // Read a chunk of the file + wfSuppressWarnings(); $f = fopen( $file, "rt" ); + wfRestoreWarnings(); if( !$f ) return "unknown/unknown"; $head = fread( $f, 1024 ); fclose( $f ); @@ -369,67 +389,88 @@ class MimeMagic { $mime = "application/x-msmetafile"; } - if (strpos($mime,"text/")===0 || $mime==="application/xml") { + if ( strpos( $mime, "text/" ) === 0 || $mime === "application/xml" ) { - $xml_type= NULL; - $script_type= NULL; + $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)=="%sim',$head,$match)) $doctype= $match[1]; - if (preg_match('%<(\w+).*>%sim',$head,$match)) $tag= $match[1]; + if ( preg_match( '%%sim', + $head, $match ) ) { + $doctype = $match[1]; + } + if ( preg_match( '%<(\w+).*>%sim', $head, $match ) ) { + $tag = $match[1]; + } #print "
    ANALYSING $file ($mime): doctype= $doctype; tag= $tag
    "; - 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"; + 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 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 ( !$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); + if ( $script_type ) { + if ( $script_type !== "UTF-8" && $script_type !== "ASCII") { + $head = iconv( $script_type, "ASCII//IGNORE", $head); + } - $match= array(); - $prog= ""; + $match = array(); - if (preg_match('%/?([^\s]+/)(w+)%sim',$head,$match)) { - $script= $match[2]; // FIXME: $script variable not used; should this be "$prog = $match[2];" instead? + if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) { + $mime = "application/x-{$match[2]}"; } - - $mime= "application/x-$prog"; } } @@ -450,42 +491,43 @@ class MimeMagic { ( strpos( $head, "<\x00?\x00\t" ) !== false ) || ( strpos( $head, "<\x00?\x00=" ) !== false ) ) { - $mime= "application/x-php"; + $mime = "application/x-php"; } } } - if (isset($this->mMimeTypeAliases[$mime])) $mime= $this->mMimeTypeAliases[$mime]; + if ( isset( $this->mMimeTypeAliases[$mime] ) ) { + $mime = $this->mMimeTypeAliases[$mime]; + } - wfDebug("$fname: final mime type of $file: $mime\n"); + wfDebug(__METHOD__.": final mime type of $file: $mime\n"); return $mime; } /** Internal mime type detection, please use guessMimeType() for application code instead. * Detection is done using an external program, if $wgMimeDetectorCommand is set. * Otherwise, the fileinfo extension and mime_content_type are tried (in this order), if they are available. - * If the dections fails and $useExt is true, the mime type is guessed from the file extension, using guessTypesForExtension. + * If the dections fails and $ext is not false, the mime type is guessed from the file extension, using + * guessTypesForExtension. * If the mime type is still unknown, getimagesize is used to detect the mime type if the file is an image. * If no mime type can be determined, this function returns "unknown/unknown". * * @param string $file The file to check - * @param bool $useExt switch for allowing to use the file extension to guess the mime type. true by default. + * @param mixed $ext The file extension, or true to extract it from the filename. + * Set it to false to ignore the extension. * * @return string the mime type of $file * @access private */ - function detectMimeType( $file, $useExt=true ) { - $fname = 'MimeMagic::detectMimeType'; - + function detectMimeType( $file, $ext = true ) { global $wgMimeDetectorCommand; - $m= NULL; - if ($wgMimeDetectorCommand) { - $fn= wfEscapeShellArg($file); - $m= `$wgMimeDetectorCommand $fn`; - } - else if (function_exists("finfo_open") && function_exists("finfo_file")) { + $m = NULL; + if ( $wgMimeDetectorCommand ) { + $fn = wfEscapeShellArg( $file ); + $m = `$wgMimeDetectorCommand $fn`; + } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) { # This required the fileinfo extension by PECL, # see http://pecl.php.net/package/fileinfo @@ -500,13 +542,12 @@ class MimeMagic { $mime_magic_resource = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */ if ($mime_magic_resource) { - $m= finfo_file($mime_magic_resource, $file); - - finfo_close($mime_magic_resource); + $m = finfo_file( $mime_magic_resource, $file ); + finfo_close( $mime_magic_resource ); + } else { + wfDebug( __METHOD__.": finfo_open failed on ".FILEINFO_MIME."!\n" ); } - else wfDebug("$fname: finfo_open failed on ".FILEINFO_MIME."!\n"); - } - else if (function_exists("mime_content_type")) { + } elseif ( function_exists( "mime_content_type" ) ) { # NOTE: this function is available since PHP 4.3.0, but only if # PHP was compiled with --with-mime-magic or, before 4.3.2, with --enable-mime-magic. @@ -517,93 +558,99 @@ class MimeMagic { # Also note that this has been DEPRECATED in favor of the fileinfo extension by PECL, see above. # see http://www.php.net/manual/en/ref.mime-magic.php for details. - $m= mime_content_type($file); + $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("$fname: (re)detected $file as image/vnd.djvu\n"); + wfDebug( __METHOD__.": (re)detected $file as image/vnd.djvu\n" ); $m = 'image/vnd.djvu'; } } + } else { + wfDebug( __METHOD__.": no magic mime detector found!\n" ); } - else wfDebug("$fname: no magic mime detector found!\n"); - if ($m) { - #normalize - $m= preg_replace('![;, ].*$!','',$m); #strip charset, etc - $m= trim($m); - $m= strtolower($m); + if ( $m ) { + # normalize + $m = preg_replace( '![;, ].*$!', '', $m ); #strip charset, etc + $m = trim( $m ); + $m = strtolower( $m ); - if (strpos($m,'unknown')!==false) $m= NULL; - else { - wfDebug("$fname: magic mime type of $file: $m\n"); + if ( strpos( $m, 'unknown' ) !== false ) { + $m = NULL; + } else { + wfDebug( __METHOD__.": magic mime type of $file: $m\n" ); return $m; } } - #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? + # 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; + $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("$fname: image mime type of $file: $m\n"); + if ( $m ) { + wfDebug( __METHOD__.": image mime type of $file: $m\n" ); return $m; } - else $notAnImage= true; + else { + $notAnImage = true; + } } else { // Also test DjVu $deja = new DjVuImage( $file ); if( $deja->isValid() ) { - wfDebug("$fname: detected $file as image/vnd.djvu\n"); + wfDebug( __METHOD__.": detected $file as image/vnd.djvu\n" ); return 'image/vnd.djvu'; } } - #if desired, look at extension as a fallback. - if ($useExt) { + # if desired, look at extension as a fallback. + if ( $ext === true ) { $i = strrpos( $file, '.' ); - $e= strtolower( $i ? substr( $file, $i + 1 ) : '' ); - - $m= $this->guessTypesForExtension($e); + $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 + # 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("$fname: extension mime type of $file: $m\n"); + if ( $m ) { + wfDebug( __METHOD__.": extension mime type of $file: $m\n" ); return $m; } } #unknown type - wfDebug("$fname: failed to guess mime type for $file!\n"); + wfDebug( __METHOD__.": failed to guess mime type for $file!\n" ); return "unknown/unknown"; } @@ -623,61 +670,61 @@ class MimeMagic { * * @return (int?string?) a value to be used with the MEDIATYPE_xxx constants. */ - function getMediaType($path=NULL,$mime=NULL) { + function getMediaType( $path = NULL, $mime = NULL ) { if( !$mime && !$path ) return MEDIATYPE_UNKNOWN; - #if mime type is unknown, guess it - if( !$mime ) $mime= $this->guessMimeType($path,false); + # If mime type is unknown, guess it + if( !$mime ) $mime = $this->guessMimeType( $path, false ); - #special code for ogg - detect if it's video (theora), - #else label it as sound. - if( $mime=="application/ogg" && file_exists($path) ) { + # Special code for ogg - detect if it's video (theora), + # else label it as sound. + if( $mime == "application/ogg" && file_exists( $path ) ) { // Read a chunk of the file $f = fopen( $path, "rt" ); - if( !$f ) return MEDIATYPE_UNKNOWN; + if ( !$f ) return MEDIATYPE_UNKNOWN; $head = fread( $f, 256 ); fclose( $f ); - $head= strtolower( $head ); + $head = strtolower( $head ); - #This is an UGLY HACK, file should be parsed correctly - if( strpos($head,'theora')!==false ) return MEDIATYPE_VIDEO; - elseif( strpos($head,'vorbis')!==false ) return MEDIATYPE_AUDIO; - elseif( strpos($head,'flac')!==false ) return MEDIATYPE_AUDIO; - elseif( strpos($head,'speex')!==false ) return MEDIATYPE_AUDIO; + # This is an UGLY HACK, file should be parsed correctly + if ( strpos( $head, 'theora' ) !== false ) return MEDIATYPE_VIDEO; + elseif ( strpos( $head, 'vorbis' ) !== false ) return MEDIATYPE_AUDIO; + elseif ( strpos( $head, 'flac' ) !== false ) return MEDIATYPE_AUDIO; + elseif ( strpos( $head, 'speex' ) !== false ) return MEDIATYPE_AUDIO; else return MEDIATYPE_MULTIMEDIA; } - #check for entry for full mime type + # check for entry for full mime type if( $mime ) { - $type= $this->findMediaType($mime); - if( $type!==MEDIATYPE_UNKNOWN ) return $type; + $type = $this->findMediaType( $mime ); + if( $type !== MEDIATYPE_UNKNOWN ) return $type; } - #check for entry for file extension - $e= NULL; - if( $path ) { + # Check for entry for file extension + $e = NULL; + if ( $path ) { $i = strrpos( $path, '.' ); - $e= strtolower( $i ? substr( $path, $i + 1 ) : '' ); + $e = strtolower( $i ? substr( $path, $i + 1 ) : '' ); - #TODO: look at multi-extension if this fails, parse from full path + # TODO: look at multi-extension if this fails, parse from full path - $type= $this->findMediaType('.'.$e); - if( $type!==MEDIATYPE_UNKNOWN ) return $type; + $type = $this->findMediaType( '.' . $e ); + if ( $type !== MEDIATYPE_UNKNOWN ) return $type; } - #check major mime type + # Check major mime type if( $mime ) { - $i= strpos($mime,'/'); + $i = strpos( $mime, '/' ); if( $i !== false ) { - $major= substr($mime,0,$i); - $type= $this->findMediaType($major); - if( $type!==MEDIATYPE_UNKNOWN ) return $type; + $major = substr( $mime, 0, $i ); + $type = $this->findMediaType( $major ); + if( $type !== MEDIATYPE_UNKNOWN ) return $type; } } - if( !$type ) $type= MEDIATYPE_UNKNOWN; + if( !$type ) $type = MEDIATYPE_UNKNOWN; return $type; } @@ -689,25 +736,26 @@ class MimeMagic { * This funktion relies on the mapping defined by $this->mMediaTypes * @access private */ - function findMediaType($extMime) { - - if (strpos($extMime,'.')===0) { #if it's an extension, look up the mime types - $m= $this->getTypesForExtension(substr($extMime,1)); - if (!$m) return MEDIATYPE_UNKNOWN; - - $m= explode(' ',$m); - } - else { #normalize mime type - if (isset($this->mMimeTypeAliases[$extMime])) { - $extMime= $this->mMimeTypeAliases[$extMime]; + function findMediaType( $extMime ) { + if ( strpos( $extMime, '.' ) === 0 ) { #if it's an extension, look up the mime types + $m = $this->getTypesForExtension( substr( $extMime, 1 ) ); + if ( !$m ) return MEDIATYPE_UNKNOWN; + + $m = explode( ' ', $m ); + } else { + # Normalize mime type + if ( isset( $this->mMimeTypeAliases[$extMime] ) ) { + $extMime = $this->mMimeTypeAliases[$extMime]; } - $m= array($extMime); + $m = array($extMime); } - foreach ($m as $mime) { - foreach ($this->mMediaTypes as $type => $codes) { - if (in_array($mime,$codes,true)) return $type; + foreach ( $m as $mime ) { + foreach ( $this->mMediaTypes as $type => $codes ) { + if ( in_array($mime, $codes, true ) ) { + return $type; + } } } @@ -715,4 +763,4 @@ class MimeMagic { } } -?> + diff --git a/includes/Namespace.php b/includes/Namespace.php index dd67b55a..f4df3bac 100644 --- a/includes/Namespace.php +++ b/includes/Namespace.php @@ -44,54 +44,67 @@ if( is_array( $wgExtraNamespaces ) ) { class Namespace { /** - * Check if the given namespace might be moved + * Can pages in the given namespace be moved? + * + * @param int $index Namespace index * @return bool */ - static function isMovable( $index ) { + public static function isMovable( $index ) { return !( $index < NS_MAIN || $index == NS_IMAGE || $index == NS_CATEGORY ); } /** - * Check if the given namespace is not a talk page + * Is the given namespace is a subject (non-talk) namespace? + * + * @param int $index Namespace index * @return bool */ - static function isMain( $index ) { - return ! Namespace::isTalk( $index ); + public static function isMain( $index ) { + return !self::isTalk( $index ); } /** - * Check if the give namespace is a talk page + * Is the given namespace a talk namespace? + * + * @param int $index Namespace index * @return bool */ - static function isTalk( $index ) { - return ($index > NS_MAIN) // Special namespaces are negative - && ($index % 2); // Talk namespaces are odd-numbered + public static function isTalk( $index ) { + return $index > NS_MAIN + && $index % 2; } /** - * Get the talk namespace corresponding to the given index + * Get the talk namespace index for a given namespace + * + * @param int $index Namespace index + * @return int */ - static function getTalk( $index ) { - if ( Namespace::isTalk( $index ) ) { - return $index; - } else { - # FIXME - return $index + 1; - } + public static function getTalk( $index ) { + return self::isTalk( $index ) + ? $index + : $index + 1; } - static function getSubject( $index ) { - if ( Namespace::isTalk( $index ) ) { - return $index - 1; - } else { - return $index; - } + /** + * Get the subject namespace index for a given namespace + * + * @param int $index Namespace index + * @return int + */ + public static function getSubject( $index ) { + return self::isTalk( $index ) + ? $index - 1 + : $index; } /** * Returns the canonical (English Wikipedia) name for a given index + * + * @param int $index Namespace index + * @return string */ - static function getCanonicalName( $index ) { + public static function getCanonicalName( $index ) { global $wgCanonicalNamespaceNames; return $wgCanonicalNamespaceNames[$index]; } @@ -99,8 +112,11 @@ class Namespace { /** * Returns the index for a given canonical name, or NULL * The input *must* be converted to lower case first + * + * @param string $name Namespace name + * @return int */ - static function getCanonicalIndex( $name ) { + public static function getCanonicalIndex( $name ) { global $wgCanonicalNamespaceNames; static $xNamespaces = false; if ( $xNamespaces === false ) { @@ -118,10 +134,12 @@ class Namespace { /** * Can this namespace ever have a talk namespace? + * * @param $index Namespace index + * @return bool */ - static function canTalk( $index ) { - return( $index >= NS_MAIN ); + public static function canTalk( $index ) { + return $index >= NS_MAIN; } /** @@ -134,8 +152,16 @@ class Namespace { public static function isContent( $index ) { global $wgContentNamespaces; return $index == NS_MAIN || in_array( $index, $wgContentNamespaces ); - } + } + + /** + * Can pages in a namespace be watched? + * + * @param int $index + * @return bool + */ + public static function isWatchable( $index ) { + return $index >= NS_MAIN; + } -} - -?> +} \ No newline at end of file diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php index 3b43dd53..7d9caf8a 100644 --- a/includes/ObjectCache.php +++ b/includes/ObjectCache.php @@ -71,7 +71,7 @@ function &wfGetCache( $inputType ) { } elseif ( function_exists( 'apc_fetch') ) { $wgCaches[CACHE_ACCEL] = new APCBagOStuff; } elseif( function_exists( 'xcache_get' ) ) { - $wgCaches[CACHE_ACCEL] = new XCacheBagOStuff; + $wgCaches[CACHE_ACCEL] = new XCacheBagOStuff(); } elseif ( function_exists( 'mmcache_get' ) ) { $wgCaches[CACHE_ACCEL] = new TurckBagOStuff; } else { @@ -123,4 +123,4 @@ function &wfGetParserCacheStorage() { return $ret; } -?> + diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index d7e7c90f..d8ac12b5 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -17,31 +17,68 @@ function wfOutputHandler( $s ) { return $s; } +/** + * Get the "file extension" that some client apps will estimate from + * the currently-requested URL. + * This isn't on WebRequest because we need it when things aren't initialized + * @private + */ +function wfRequestExtension() { + /// @fixme -- this sort of dupes some code in WebRequest::getRequestUrl() + if( isset( $_SERVER['REQUEST_URI'] ) ) { + // Strip the query string... + list( $path ) = explode( '?', $_SERVER['REQUEST_URI'], 2 ); + } elseif( isset( $_SERVER['SCRIPT_NAME'] ) ) { + // Probably IIS. QUERY_STRING appears separately. + $path = $_SERVER['SCRIPT_NAME']; + } else { + // Can't get the path from the server? :( + return ''; + } + + $period = strrpos( $path, '.' ); + if( $period !== false ) { + return strtolower( substr( $path, $period ) ); + } + return ''; +} + /** * Handler that compresses data with gzip if allowed by the Accept header. * Unlike ob_gzhandler, it works for HEAD requests too. */ function wfGzipHandler( $s ) { - if ( function_exists( 'gzencode' ) && !headers_sent() ) { - $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 - $headers = headers_list(); - $foundVary = false; - foreach ( $headers as $header ) { - if ( substr( $header, 0, 5 ) == 'Vary:' ) { - $foundVary = true; - break; - } - } - if ( !$foundVary ) { - header( 'Vary: Accept-Encoding' ); - } + if( !function_exists( 'gzencode' ) || headers_sent() ) { + return $s; + } + + $ext = wfRequestExtension(); + if( $ext == '.gz' || $ext == '.tgz' ) { + // Don't do gzip compression if the URL path ends in .gz or .tgz + // This confuses Safari and triggers a download of the page, + // even though it's pretty clearly labeled as viewable HTML. + // Bad Safari! Bad! + return $s; + } + + $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 + $headers = headers_list(); + $foundVary = false; + foreach ( $headers as $header ) { + if ( substr( $header, 0, 5 ) == 'Vary:' ) { + $foundVary = true; + break; } } + if ( !$foundVary ) { + header( 'Vary: Accept-Encoding' ); + } return $s; } @@ -61,4 +98,4 @@ function wfDoContentLength( $length ) { } } -?> + diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 03e832a4..06467157 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -15,6 +15,7 @@ class OutputPage { var $mLastModified, $mETag, $mCategoryLinks; var $mScripts, $mLinkColours, $mPageLinkTitle; + var $mAllowUserJs; var $mSuppressQuickbar; var $mOnloadHandler; var $mDoNothing; @@ -27,12 +28,15 @@ class OutputPage { var $mNewSectionLink = false; var $mNoGallery = false; + var $mPageTitleActionText = ''; /** * Constructor * Initialise private variables */ function __construct() { + global $wgAllowUserJs; + $this->mAllowUserJs = $wgAllowUserJs; $this->mMetatags = $this->mKeywords = $this->mLinktags = array(); $this->mHTMLtitle = $this->mPagetitle = $this->mBodytext = $this->mRedirect = $this->mLastModified = @@ -51,6 +55,7 @@ class OutputPage { $this->mETag = false; $this->mRevisionId = null; $this->mNewSectionLink = false; + $this->mTemplateIds = array(); } public function redirect( $url, $responsecode = '302' ) { @@ -71,6 +76,13 @@ class OutputPage { function addMeta( $name, $val ) { array_push( $this->mMetatags, array( $name, $val ) ); } function addKeyword( $text ) { array_push( $this->mKeywords, $text ); } function addScript( $script ) { $this->mScripts .= "\t\t".$script; } + function addStyle( $style ) { + global $wgStylePath, $wgStyleVersion; + $this->addLink( + array( + 'rel' => 'stylesheet', + 'href' => $wgStylePath . '/' . $style . '?' . $wgStyleVersion ) ); + } /** * Add a self-contained script tag with the given contents @@ -97,6 +109,10 @@ class OutputPage { $this->mHeadItems[$name] = $value; } + function hasHeadItem( $name ) { + return isset( $this->mHeadItems[$name] ); + } + function setETag($tag) { $this->mETag = $tag; } function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; } function getArticleBodyOnly($only) { return $this->mArticleBodyOnly; } @@ -146,7 +162,11 @@ class OutputPage { # Wed, 20 Aug 2003 06:51:19 GMT; length=5202 # this breaks strtotime(). $modsince = preg_replace( '/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"] ); + + wfSuppressWarnings(); // E_STRICT system time bitching $modsinceTime = strtotime( $modsince ); + wfRestoreWarnings(); + $ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 ); wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); @@ -174,26 +194,13 @@ class OutputPage { } } + function setPageTitleActionText( $text ) { + $this->mPageTitleActionText = $text; + } + function getPageTitleActionText () { - global $action; - switch($action) { - case 'edit': - case 'delete': - case 'protect': - case 'unprotect': - case 'watch': - case 'unwatch': - // Display title is already customized - return ''; - case 'history': - return wfMsg('history_short'); - case 'submit': - // FIXME: bug 2735; not correct for special pages etc - return wfMsg('preview'); - case 'info': - return wfMsg('info_short'); - default: - return ''; + if ( isset( $this->mPageTitleActionText ) ) { + return $this->mPageTitleActionText; } } @@ -283,6 +290,9 @@ class OutputPage { public function suppressQuickbar() { $this->mSuppressQuickbar = true; } public function isQuickbarSuppressed() { return $this->mSuppressQuickbar; } + public function disallowUserJs() { $this->mAllowUserJs = false; } + public function isUserJsAllowed() { return $this->mAllowUserJs; } + public function addHTML( $text ) { $this->mBodytext .= $text; } public function clearHTML() { $this->mBodytext = ''; } public function getHTML() { return $this->mBodytext; } @@ -363,14 +373,24 @@ class OutputPage { if ( $parserOutput->getCacheTime() == -1 ) { $this->enableClientCache( false ); } - if ( $parserOutput->mHTMLtitle != "" ) { - $this->mPagetitle = $parserOutput->mHTMLtitle ; - } - if ( $parserOutput->mSubtitle != '' ) { - $this->mSubtitle .= $parserOutput->mSubtitle ; - } $this->mNoGallery = $parserOutput->getNoGallery(); $this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems ); + // Versioning... + $this->mTemplateIds += (array)$parserOutput->mTemplateIds; + + # Display title + if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) + $this->setPageTitle( $dt ); + + # Hooks registered in the object + global $wgParserOutputHooks; + foreach ( $parserOutput->getOutputHooks() as $hookInfo ) { + list( $hookName, $data ) = $hookInfo; + if ( isset( $wgParserOutputHooks[$hookName] ) ) { + call_user_func( $wgParserOutputHooks[$hookName], $this, $parserOutput, $data ); + } + } + wfRunHooks( 'OutputPageParserOutput', array( &$this, $parserOutput ) ); } @@ -730,27 +750,51 @@ class OutputPage { * @return nothing */ function blockedPage( $return = true ) { - global $wgUser, $wgContLang, $wgTitle; + global $wgUser, $wgContLang, $wgTitle, $wgLang; $this->setPageTitle( wfMsg( 'blockedtitle' ) ); $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); - $id = $wgUser->blockedBy(); + $name = User::whoIs( $wgUser->blockedBy() ); $reason = $wgUser->blockedFor(); + $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true ); $ip = wfGetIP(); - if ( is_numeric( $id ) ) { - $name = User::whoIs( $id ); - } else { - $name = $id; - } $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; $blockid = $wgUser->mBlock->mId; - $this->addWikiText( wfMsg( 'blockedtext', $link, $reason, $ip, $name, $blockid ) ); - + $blockExpiry = $wgUser->mBlock->mExpiry; + if ( $blockExpiry == 'infinity' ) { + // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite' + // Search for localization in 'ipboptions' + $scBlockExpiryOptions = wfMsg( 'ipboptions' ); + foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) { + if ( strpos( $option, ":" ) === false ) + continue; + list( $show, $value ) = explode( ":", $option ); + if ( $value == 'infinite' || $value == 'indefinite' ) { + $blockExpiry = $show; + break; + } + } + } else { + $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); + } + + if ( $wgUser->mBlock->mAuto ) { + $msg = 'autoblockedtext'; + } else { + $msg = 'blockedtext'; + } + + /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked. + * 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 ) ); + # Don't auto-return to special pages if( $return ) { $return = $wgTitle->getNamespace() > -1 ? $wgTitle->getPrefixedText() : NULL; @@ -759,13 +803,13 @@ class OutputPage { } /** - * Outputs a pretty page to explain why the request exploded. + * Output a standard error page * - * @param string $title Message key for page title. - * @param string $msg Message key for page text. - * @return nothing + * @param string $title Message key for page title + * @param string $msg Message key for page text + * @param array $params Message parameters */ - public function showErrorPage( $title, $msg ) { + public function showErrorPage( $title, $msg, $params = array() ) { global $wgTitle; $this->mDebugtext .= 'Original title: ' . @@ -776,12 +820,36 @@ class OutputPage { $this->setArticleRelated( false ); $this->enableClientCache( false ); $this->mRedirect = ''; - $this->mBodytext = ''; - $this->addWikiText( wfMsg( $msg ) ); + + array_unshift( $params, 'parse' ); + array_unshift( $params, $msg ); + $this->addHtml( call_user_func_array( 'wfMsgExt', $params ) ); + $this->returnToMain( false ); } + /** + * Output a standard permission error page + * + * @param array $errors Error message keys + */ + public function showPermissionsErrorPage( $errors ) + { + global $wgTitle; + + $this->mDebugtext .= 'Original title: ' . + $wgTitle->getPrefixedText() . "\n"; + $this->setPageTitle( wfMsg( 'permissionserrors' ) ); + $this->setHTMLTitle( wfMsg( 'permissionserrors' ) ); + $this->setRobotpolicy( 'noindex,nofollow' ); + $this->setArticleRelated( false ); + $this->enableClientCache( false ); + $this->mRedirect = ''; + $this->mBodytext = ''; + $this->addWikiText( $this->formatPermissionsErrorMessage( $errors ) ); + } + /** @deprecated */ public function errorpage( $title, $msg ) { throw new ErrorPageError( $title, $msg ); @@ -897,39 +965,76 @@ class OutputPage { throw new MWException( "OutputPage::databaseError is obsolete\n" ); } + /** + * @param array $errors An array of arrays returned by Title::getUserPermissionsErrors + * @return string The error-messages, formatted into a list. + */ + public function formatPermissionsErrorMessage( $errors ) { + $text = ''; + + if (sizeof( $errors ) > 1) { + + $text .= wfMsgExt( 'permissionserrorstext', array( 'parse' ), count( $errors ) ) . "\n"; + $text .= '
      ' . "\n"; + + foreach( $errors as $error ) + { + $text .= '
    • '; + $text .= call_user_func_array( 'wfMsg', $error ); + $text .= "
    • \n"; + } + $text .= '
    '; + } else { + $text .= call_user_func_array( 'wfMsg', $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 ) */ - public function readOnlyPage( $source = null, $protected = false ) { + public function readOnlyPage( $source = null, $protected = false, $reasons = array() ) { global $wgUser, $wgReadOnlyFile, $wgReadOnly, $wgTitle; $skin = $wgUser->getSkin(); $this->setRobotpolicy( 'noindex,nofollow' ); $this->setArticleRelated( false ); - - if( $protected ) { + + if ( !empty($reasons) ) { $this->setPageTitle( wfMsg( 'viewsource' ) ); $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); - list( $cascadeSources, $restrictions ) = $wgTitle->getCascadeProtectionSources(); + $this->addWikiText( $this->formatPermissionsErrorMessage( $reasons ) ); + } else if( $protected ) { + $this->setPageTitle( wfMsg( 'viewsource' ) ); + $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) ); + list( $cascadeSources, /* $restrictions */ ) = $wgTitle->getCascadeProtectionSources(); - # Determine if protection is due to the page being a system message - # and show an appropriate explanation + // 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' ) ); - } if ( $cascadeSources && count($cascadeSources) > 0 ) { - $titles = ''; - - foreach ( $cascadeSources as $title ) { - $titles .= '* [[:' . $title->getPrefixedText() . "]]\n"; - } - - $notice = wfMsgExt( 'cascadeprotected', array('parsemag'), count($cascadeSources) ) . "\n$titles"; - - $this->addWikiText( $notice ); + } 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 ) ); } else { + // Standard protection $this->addWikiText( wfMsg( 'protectedpagetext' ) ); } } else { @@ -950,8 +1055,8 @@ class OutputPage { htmlspecialchars( $source ) . "\n"; $this->addHTML( $text ); } - $article = new Article($wgTitle); - $this->addHTML( $skin->formatTemplates($article->getUsedTemplates()) ); + $article = new Article( $wgTitle ); + $this->addHTML( $skin->formatTemplates( $article->getUsedTemplates() ) ); $this->returnToMain( false ); } @@ -1016,12 +1121,25 @@ class OutputPage { } /** - * return from error messages or notes - * @param $auto automatically redirect the user after 10 seconds - * @param $returnto page title to return to. Default is Main Page. + * Add a "return to" link pointing to a specified title + * + * @param Title $title Title to link + */ + public function addReturnTo( $title ) { + global $wgUser; + $link = wfMsg( 'returnto', $wgUser->getSkin()->makeLinkObj( $title ) ); + $this->addHtml( "

    {$link}

    \n" ); + } + + /** + * Add a "return to" link pointing to a specified title, + * or the title indicated in the request, or else the main page + * + * @param null $unused No longer used + * @param Title $returnto Title to return to */ - public function returnToMain( $auto = true, $returnto = NULL ) { - global $wgUser, $wgOut, $wgRequest; + public function returnToMain( $unused = null, $returnto = NULL ) { + global $wgRequest; if ( $returnto == NULL ) { $returnto = $wgRequest->getText( 'returnto' ); @@ -1040,14 +1158,7 @@ class OutputPage { $titleObj = Title::newMainPage(); } - $sk = $wgUser->getSkin(); - $link = $sk->makeLinkObj( $titleObj, '' ); - - $r = wfMsg( 'returnto', $link ); - if ( $auto ) { - $wgOut->addMeta( 'http:Refresh', '10;url=' . $titleObj->escapeFullURL() ); - } - $wgOut->addHTML( "\n

    $r

    \n" ); + $this->addReturnTo( $titleObj ); } /** @@ -1114,7 +1225,7 @@ class OutputPage { $ret .= "\n"; $sk = $wgUser->getSkin(); - $ret .= $sk->getHeadScripts(); + $ret .= $sk->getHeadScripts( $this->mAllowUserJs ); $ret .= $this->mScripts; $ret .= $sk->getUserStyles(); $ret .= $this->getHeadItems(); @@ -1191,10 +1302,30 @@ class OutputPage { /** * Show an "add new section" link? * - * @return bool True if the parser output instructs us to add one + * @return bool */ public function showNewSectionLink() { return $this->mNewSectionLink; } + + /** + * Show a warning about slave lag + * + * If the lag is higher than $wgSlaveLagCritical seconds, + * then the warning is a bit more obvious. If the lag is + * lower than $wgSlaveLagWarning, then no warning is shown. + * + * @param int $lag Slave lag + */ + public function showLagWarning( $lag ) { + global $wgSlaveLagWarning, $wgSlaveLagCritical; + if( $lag >= $wgSlaveLagWarning ) { + $message = $lag < $wgSlaveLagCritical + ? 'lag-warn-normal' + : 'lag-warn-high'; + $warning = wfMsgExt( $message, 'parse', $lag ); + $this->addHtml( "
    \n{$warning}\n
    \n" ); + } + } + } -?> diff --git a/includes/PageHistory.php b/includes/PageHistory.php index b1cf41f0..d84c3515 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -62,6 +62,7 @@ class PageHistory { * Setup page variables. */ $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); + $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) ); $wgOut->setArticleFlag( false ); $wgOut->setArticleRelated( true ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); @@ -244,8 +245,26 @@ class PageHistory { if( $row->rev_deleted & Revision::DELETED_TEXT ) { $s .= ' ' . wfMsgHtml( 'deletedrev' ); } - if( $wgUser->isAllowed( 'rollback' ) && $latest ) { - $s .= ' '.$this->mSkin->generateRollback( $rev ); + + $tools = array(); + + if ( !is_null( $next ) && is_object( $next ) ) { + if( $wgUser->isAllowed( 'rollback' ) && $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( $tools ) { + $s .= ' (' . implode( ' | ', $tools ) . ')'; } wfRunHooks( 'PageHistoryLineEnding', array( &$row , &$s ) ); @@ -589,4 +608,5 @@ class PageHistoryPager extends ReverseChronologicalPager { } } -?> + + diff --git a/includes/PageQueryPage.php b/includes/PageQueryPage.php index 5b82ebf6..53b7765e 100644 --- a/includes/PageQueryPage.php +++ b/includes/PageQueryPage.php @@ -23,4 +23,4 @@ class PageQueryPage extends QueryPage { } } -?> + diff --git a/includes/Pager.php b/includes/Pager.php index a475dc16..70d0873c 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -12,41 +12,42 @@ interface Pager { /** * IndexPager is an efficient pager which uses a (roughly unique) index in the * data set to implement paging, rather than a "LIMIT offset,limit" clause. - * In MySQL, such a limit/offset clause requires counting through the specified number - * of offset rows to find the desired data, which can be expensive for large offsets. + * In MySQL, such a limit/offset clause requires counting through the + * specified number of offset rows to find the desired data, which can be + * expensive for large offsets. * - * ReverseChronologicalPager is a child class of the abstract IndexPager, and contains - * some formatting and display code which is specific to the use of timestamps as - * indexes. Here is a synopsis of its operation: + * ReverseChronologicalPager is a child class of the abstract IndexPager, and + * contains some formatting and display code which is specific to the use of + * timestamps as indexes. Here is a synopsis of its operation: * - * * The query is specified by the offset, limit and direction (dir) parameters, in - * addition to any subclass-specific parameters. + * * The query is specified by the offset, limit and direction (dir) + * parameters, in addition to any subclass-specific parameters. + * * The offset is the non-inclusive start of the DB query. A row with an + * index value equal to the offset will never be shown. + * * The query may either be done backwards, where the rows are returned by + * the database in the opposite order to which they are displayed to the + * user, or forwards. This is specified by the "dir" parameter, dir=prev + * means backwards, anything else means forwards. The offset value + * specifies the start of the database result set, which may be either + * the start or end of the displayed data set. This allows "previous" + * links to be implemented without knowledge of the index value at the + * start of the previous page. + * * An additional row beyond the user-specified limit is always requested. + * This allows us to tell whether we should display a "next" link in the + * case of forwards mode, or a "previous" link in the case of backwards + * mode. Determining whether to display the other link (the one for the + * page before the start of the database result set) can be done + * heuristically by examining the offset. * - * * The offset is the non-inclusive start of the DB query. A row with an index value - * equal to the offset will never be shown. + * * An empty offset indicates that the offset condition should be omitted + * from the query. This naturally produces either the first page or the + * last page depending on the dir parameter. * - * * The query may either be done backwards, where the rows are returned by the database - * in the opposite order to which they are displayed to the user, or forwards. This is - * specified by the "dir" parameter, dir=prev means backwards, anything else means - * forwards. The offset value specifies the start of the database result set, which - * may be either the start or end of the displayed data set. This allows "previous" - * links to be implemented without knowledge of the index value at the start of the - * previous page. - * - * * An additional row beyond the user-specified limit is always requested. This allows - * us to tell whether we should display a "next" link in the case of forwards mode, - * or a "previous" link in the case of backwards mode. Determining whether to - * display the other link (the one for the page before the start of the database - * result set) can be done heuristically by examining the offset. - * - * * An empty offset indicates that the offset condition should be omitted from the query. - * This naturally produces either the first page or the last page depending on the - * dir parameter. - * - * Subclassing the pager to implement concrete functionality should be fairly simple, - * please see the examples in PageHistory.php and SpecialIpblocklist.php. You just need - * to override formatRow(), getQueryInfo() and getIndexField(). Don't forget to call the - * parent constructor if you override it. + * Subclassing the pager to implement concrete functionality should be fairly + * simple, please see the examples in PageHistory.php and + * SpecialIpblocklist.php. You just need to override formatRow(), + * getQueryInfo() and getIndexField(). Don't forget to call the parent + * constructor if you override it. * * @addtogroup Pager */ @@ -75,9 +76,9 @@ abstract class IndexPager implements Pager { global $wgRequest, $wgUser; $this->mRequest = $wgRequest; - # NB: the offset is quoted, not validated. It is treated as an arbitrary string - # to support the widest variety of index types. Be careful outputting it into - # HTML! + # NB: the offset is quoted, not validated. It is treated as an + # arbitrary string to support the widest variety of index types. Be + # careful outputting it into HTML! $this->mOffset = $this->mRequest->getText( 'offset' ); # Use consistent behavior for the limit options @@ -106,6 +107,9 @@ abstract class IndexPager implements Pager { $this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending ); $this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult ); $this->mQueryDone = true; + + $this->preprocessResults( $this->mResult ); + $this->mResult->rewind(); // Paranoia wfProfileOut( $fname ); } @@ -131,9 +135,10 @@ abstract class IndexPager implements Pager { $lastIndex = $row[$this->mIndexField]; } else { $this->mPastTheEndRow = null; - # Setting indexes to an empty string means that they will be omitted - # if they would otherwise appear in URLs. It just so happens that this - # is the right thing to do in the standard UI, in all the relevant cases. + # Setting indexes to an empty string means that they will be + # omitted if they would otherwise appear in URLs. It just so + # happens that this is the right thing to do in the standard + # UI, in all the relevant cases. $this->mPastTheEndIndex = ''; $res->seek( $numRows - 1 ); $row = $res->fetchRow(); @@ -160,21 +165,22 @@ abstract class IndexPager implements Pager { } /** - * Do a query with specified parameters, rather than using the object context + * Do a query with specified parameters, rather than using the object + * context * * @param string $offset Index offset, inclusive * @param integer $limit Exact query limit * @param boolean $descending Query direction, false for ascending, true for descending * @return ResultWrapper */ - function reallyDoQuery( $offset, $limit, $ascending ) { + function reallyDoQuery( $offset, $limit, $descending ) { $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; $info = $this->getQueryInfo(); $tables = $info['tables']; $fields = $info['fields']; $conds = isset( $info['conds'] ) ? $info['conds'] : array(); $options = isset( $info['options'] ) ? $info['options'] : array(); - if ( $ascending ) { + if ( $descending ) { $options['ORDER BY'] = $this->mIndexField; $operator = '>'; } else { @@ -189,6 +195,13 @@ abstract class IndexPager implements Pager { return new ResultWrapper( $this->mDb, $res ); } + /** + * Pre-process results; useful for performing batch existence checks, etc. + * + * @param ResultWrapper $result Result wrapper + */ + protected function preprocessResults( $result ) {} + /** * Get the formatted result list. Calls getStartBody(), formatRow() and * getEndBody(), concatenates the results and returns them. @@ -331,9 +344,10 @@ abstract class IndexPager implements Pager { } /** - * Get paging links. If a link is disabled, the item from $disabledTexts will - * be used. If there is no such item, the unlinked text from $linkTexts will - * be used. Both $linkTexts and $disabledTexts are arrays of HTML. + * Get paging links. If a link is disabled, the item from $disabledTexts + * will be used. If there is no such item, the unlinked text from + * $linkTexts will be used. Both $linkTexts and $disabledTexts are arrays + * of HTML. */ function getPagingLinks( $linkTexts, $disabledTexts = array() ) { $queries = $this->getPagingQueries(); @@ -667,20 +681,22 @@ abstract class TablePager extends IndexPager { } /** - * Return true if the named field should be sortable by the UI, false otherwise + * Return true if the named field should be sortable by the UI, false + * otherwise + * * @param string $field */ abstract function isFieldSortable( $field ); /** - * Format a table cell. The return value should be HTML, but use an empty string - * not   for empty cells. Do not include the
    and and
    \n"; - global $wgEnableCascadingProtection; - - if ($wgEnableCascadingProtection) - $out .= $this->buildCascadeInput(); - $out .= "\n"; $out .= "\n"; + global $wgEnableCascadingProtection; + if( $wgEnableCascadingProtection ) + $out .= '\n"; + $out .= $this->buildExpiryInput(); if( !$this->disabled ) { $out .= "\n"; + $out .= "\n"; $out .= "\n"; } @@ -270,22 +278,28 @@ class ProtectionForm { $out = wfOpenElement( 'select', $attribs ); foreach( $wgRestrictionLevels as $key ) { - $out .= $this->buildOption( $key, $selected ); + $out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected ); } $out .= "\n"; return $out; } - function buildOption( $key, $selected ) { - $text = ( $key == '' ) - ? wfMsg( 'protect-default' ) - : wfMsg( "protect-level-$key" ); - $selectedAttrib = ($selected == $key) - ? array( 'selected' => 'selected' ) - : array(); - return wfElement( 'option', - array( 'value' => $key ) + $selectedAttrib, - $text ); + /** + * Prepare the label for a protection selector option + * + * @param string $permission Permission required + * @return string + */ + private function getOptionLabel( $permission ) { + if( $permission == '' ) { + return wfMsg( 'protect-default' ); + } else { + $key = "protect-level-{$permission}"; + $msg = wfMsg( $key ); + if( wfEmptyMsg( $key, $msg ) ) + $msg = wfMsg( 'protect-fallback', $permission ); + return $msg; + } } function buildReasonInput() { @@ -309,22 +323,21 @@ class ProtectionForm { } function buildExpiryInput() { - $id = 'mwProtect-expiry'; - - $ci = ""; - - return $ci; + $attribs = array( 'id' => 'expires' ) + $this->disabledAttrib; + return '' + . '' + . '' + . ''; + } + + function buildWatchInput() { + global $wgUser; + return Xml::checkLabel( + wfMsg( 'watchthis' ), + 'mwProtectWatch', + 'mwProtectWatch', + $this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) + ); } function buildSubmit() { @@ -360,7 +373,7 @@ class ProtectionForm { * @access private */ function showLogExtract( &$out ) { - # Show relevant lines from the deletion log: + # Show relevant lines from the protection log: $out->addHTML( "

    " . htmlspecialchars( LogPage::logName( 'protect' ) ) . "

    \n" ); $logViewer = new LogViewer( new LogReader( @@ -369,6 +382,5 @@ class ProtectionForm { 'type' => 'protect' ) ) ) ); $logViewer->showList( $out ); } -} -?> +} \ No newline at end of file diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php index f72b640f..6585de42 100644 --- a/includes/ProxyTools.php +++ b/includes/ProxyTools.php @@ -260,4 +260,4 @@ function wfIsAOLProxy( $ip ) { -?> + diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 143c8be6..06710b6d 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -25,6 +25,7 @@ $wgQueryPages = array( array( 'MostcategoriesPage', 'Mostcategories' ), array( 'MostimagesPage', 'Mostimages' ), array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ), + array( 'SpecialMostlinkedtemplates', 'Mostlinkedtemplates' ), array( 'MostlinkedPage', 'Mostlinked' ), array( 'MostrevisionsPage', 'Mostrevisions' ), array( 'FewestrevisionsPage', 'Fewestrevisions' ), @@ -33,6 +34,7 @@ $wgQueryPages = array( array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ), array( 'UncategorizedPagesPage', 'Uncategorizedpages' ), array( 'UncategorizedImagesPage', 'Uncategorizedimages' ), + array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ), array( 'UnusedCategoriesPage', 'Unusedcategories' ), array( 'UnusedimagesPage', 'Unusedimages' ), array( 'WantedCategoriesPage', 'Wantedcategories' ), @@ -332,7 +334,8 @@ class QueryPage { $num = $dbr->numRows($res); $this->preprocessResults( $dbr, $res ); - $sk = $wgUser->getSkin(); + + $wgOut->addHtml( XML::openElement( 'div', array('class' => 'mw-spcontent') ) ); # Top header and navigation if( $shownavigation ) { @@ -347,6 +350,7 @@ class QueryPage { # No results to show, so don't bother with "showing X of Y" etc. # -- just let the user know and give up now $wgOut->addHtml( '

    ' . wfMsgHtml( 'specialpage-empty' ) . '

    ' ); + $wgOut->addHtml( XML::closeElement( 'div' ) ); return; } } @@ -365,6 +369,8 @@ class QueryPage { if( $shownavigation ) { $wgOut->addHtml( '

    ' . $paging . '

    ' ); } + + $wgOut->addHtml( XML::closeElement( 'div' ) ); return $num; } @@ -397,7 +403,7 @@ class QueryPage { ? ' class="not-patrolled"' : ''; $html[] = $this->listoutput - ? $format + ? $line : "{$line}\n"; } } @@ -411,7 +417,7 @@ class QueryPage { ? ' class="not-patrolled"' : ''; $html[] = $this->listoutput - ? $format + ? $line : "{$line}\n"; } } @@ -428,11 +434,11 @@ class QueryPage { } function openList( $offset ) { - return "
      "; + return "\n
        \n"; } function closeList() { - return '
      '; + return "
    \n"; } /** @@ -525,4 +531,4 @@ class QueryPage { } } -?> + diff --git a/includes/RawPage.php b/includes/RawPage.php index 93484829..9df94e50 100644 --- a/includes/RawPage.php +++ b/includes/RawPage.php @@ -220,4 +220,4 @@ class RawPage { return $text; } } -?> + diff --git a/includes/RecentChange.php b/includes/RecentChange.php index fced4343..79f32d0c 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -80,6 +80,31 @@ class RecentChange return NULL; } } + + /** + * Find the first recent change matching some specific conditions + * + * @param array $conds Array of conditions + * @param mixed $fname Override the method name in profiling/logs + * @return RecentChange + */ + public static function newFromConds( $conds, $fname = false ) { + if( $fname === false ) + $fname = __METHOD__; + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + 'recentchanges', + '*', + $conds, + $fname + ); + if( $res instanceof ResultWrapper && $res->numRows() > 0 ) { + $row = $res->fetchObject(); + $res->free(); + return self::newFromRow( $row ); + } + return null; + } # Accessors @@ -195,10 +220,11 @@ class RecentChange global $wgUseEnotif; if( $wgUseEnotif ) { # this would be better as an extension hook + global $wgUser; include_once( "UserMailer.php" ); $enotif = new EmailNotification(); $title = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] ); - $enotif->notifyOnPageChange( $title, + $enotif->notifyOnPageChange( $wgUser, $title, $this->mAttribs['rc_timestamp'], $this->mAttribs['rc_comment'], $this->mAttribs['rc_minor'], @@ -209,24 +235,30 @@ class RecentChange wfRunHooks( 'RecentChange_save', array( &$this ) ); } - # Marks a certain row as patrolled - function markPatrolled( $rcid ) - { - $fname = 'RecentChange::markPatrolled'; - + /** + * Mark a given change as patrolled + * + * @param mixed $change RecentChange or corresponding rc_id + */ + public static function markPatrolled( $change ) { + $rcid = $change instanceof RecentChange + ? $change->mAttribs['rc_id'] + : $change; $dbw = wfGetDB( DB_MASTER ); - - $dbw->update( 'recentchanges', - array( /* SET */ + $dbw->update( + 'recentchanges', + array( 'rc_patrolled' => 1 - ), array( /* WHERE */ + ), + array( 'rc_id' => $rcid - ), $fname + ), + __METHOD__ ); } # Makes an entry in the database corresponding to an edit - /*static*/ function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, + public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp, $bot = "default", $ip = '', $oldSize = 0, $newSize = 0, $newId = 0) { @@ -280,10 +312,8 @@ class RecentChange * Makes an entry in the database corresponding to page creation * Note: the title object must be loaded with the new id using resetArticleID() * @todo Document parameters and return - * @public - * @static */ - public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = "default", + public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default', $ip='', $size = 0, $newId = 0 ) { if ( !$ip ) { @@ -292,7 +322,7 @@ class RecentChange $ip = ''; } } - if ( $bot == 'default' ) { + if ( $bot === 'default' ) { $bot = $user->isAllowed( 'bot' ); } @@ -331,7 +361,7 @@ class RecentChange } # Makes an entry in the database corresponding to a rename - /*static*/ function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) + public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) { if ( !$ip ) { $ip = wfGetIP(); @@ -372,17 +402,17 @@ class RecentChange $rc->save(); } - /* static */ function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { + public static function notifyMoveToNew( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, false ); } - /* static */ function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { + public static function notifyMoveOverRedirect( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='' ) { RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true ); } # A log entry is different to an edit in that previous revisions are # not kept - /*static*/ function notifyLog( $timestamp, &$title, &$user, $comment, $ip='', + public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='', $type, $action, $target, $logComment, $params ) { if ( !$ip ) { @@ -595,4 +625,4 @@ class RecentChange } } } -?> + diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php new file mode 100644 index 00000000..367d994f --- /dev/null +++ b/includes/RefreshLinksJob.php @@ -0,0 +1,48 @@ +clear(); + + if ( is_null( $this->title ) ) { + $this->error = "refreshLinks: Invalid title"; + wfProfileOut( __METHOD__ ); + return false; + } + + $revision = Revision::newFromTitle( $this->title ); + if ( !$revision ) { + $this->error = 'refreshLinks: Article not found "' . $this->title->getPrefixedDBkey() . '"'; + wfProfileOut( __METHOD__ ); + return false; + } + + wfProfileIn( __METHOD__.'-parse' ); + $options = new ParserOptions; + $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() ); + wfProfileOut( __METHOD__.'-parse' ); + wfProfileIn( __METHOD__.'-update' ); + $update = new LinksUpdate( $this->title, $parserOutput, false ); + $update->doUpdate(); + wfProfileOut( __METHOD__.'-update' ); + wfProfileOut( __METHOD__ ); + return true; + } +} + diff --git a/includes/Revision.php b/includes/Revision.php index 71f214e3..39470923 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -888,4 +888,4 @@ define( 'MW_REV_DELETED_USER', Revision::DELETED_USER ); define( 'MW_REV_DELETED_RESTRICTED', Revision::DELETED_RESTRICTED ); -?> + diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index fa5416dc..f2dcbf94 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -330,6 +330,9 @@ $wgHtmlEntityAliases = array( * @addtogroup Parser */ class Sanitizer { + const NONE = 0; + const INITIAL_NONLETTER = 1; + /** * Cleans up HTML, removes dangerous tags and attributes, and * removes HTML comments @@ -339,7 +342,7 @@ class Sanitizer { * @param array $args for the processing callback * @return string */ - static function removeHTMLtags( $text, $processCallback = null, $args = array() ) { + static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array() ) { global $wgUseTidy; static $htmlpairs, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags, @@ -349,13 +352,13 @@ class Sanitizer { if ( !$staticInitialised ) { - $htmlpairs = array( # Tags that must be closed + $htmlpairs = array_merge( $extratags, array( # Tags that must be closed 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', 'strike', 'strong', 'tt', 'var', 'div', 'center', 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre', 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u' - ); + ) ); $htmlsingle = array( 'br', 'hr', 'li', 'dt', 'dd' ); @@ -566,6 +569,7 @@ class Sanitizer { * * - Discards attributes not on a whitelist for the given element * - Unsafe style attributes are discarded + * - Invalid id attributes are reencoded * * @param array $attribs * @param string $element @@ -575,7 +579,27 @@ class Sanitizer { * @todo Check for unique id attribute :P */ static function validateTagAttributes( $attribs, $element ) { - $whitelist = array_flip( Sanitizer::attributeWhitelist( $element ) ); + return Sanitizer::validateAttributes( $attribs, + Sanitizer::attributeWhitelist( $element ) ); + } + + /** + * Take an array of attribute names and values and normalize or discard + * illegal values for the given whitelist. + * + * - Discards attributes not the given whitelist + * - Unsafe style attributes are discarded + * - Invalid id attributes are reencoded + * + * @param array $attribs + * @param array $whitelist list of allowed attribute names + * @return array + * + * @todo Check for legal values where the DTD limits things. + * @todo Check for unique id attribute :P + */ + static function validateAttributes( $attribs, $whitelist ) { + $whitelist = array_flip( $whitelist ); $out = array(); foreach( $attribs as $attribute => $value ) { if( !isset( $whitelist[$attribute] ) ) { @@ -601,6 +625,33 @@ class Sanitizer { return $out; } + /** + * Merge two sets of HTML attributes. + * Conflicting items in the second set will override those + * in the first, except for 'class' attributes which will be + * combined. + * + * @todo implement merging for other attributes such as style + * @param array $a + * @param array $b + * @return array + */ + static function mergeAttributes( $a, $b ) { + $out = array_merge( $a, $b ); + if( isset( $a['class'] ) + && isset( $b['class'] ) + && $a['class'] !== $b['class'] ) { + + $out['class'] = implode( ' ', + array_unique( + preg_split( '/\s+/', + $a['class'] . ' ' . $b['class'], + -1, + PREG_SPLIT_NO_EMPTY ) ) ); + } + return $out; + } + /** * Pick apart some CSS and check it for forbidden or unsafe structures. * Returns a sanitized string, or false if it was just too evil. @@ -730,20 +781,29 @@ class Sanitizer { * name attributes * @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute * - * @static - * - * @param string $id + * @param string $id Id to validate + * @param int $flags Currently only two values: Sanitizer::INITIAL_NONLETTER + * (default) permits initial non-letter characters, + * such as if you're adding a prefix to them. + * Sanitizer::NONE will prepend an 'x' if the id + * would otherwise start with a nonletter. * @return string */ - static function escapeId( $id ) { + static function escapeId( $id, $flags = Sanitizer::INITIAL_NONLETTER ) { static $replace = array( '%3A' => ':', '%' => '.' ); $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) ); - - return str_replace( array_keys( $replace ), array_values( $replace ), $id ); + $id = str_replace( array_keys( $replace ), array_values( $replace ), $id ); + + if( ~$flags & Sanitizer::INITIAL_NONLETTER + && !preg_match( '/[a-zA-Z]/', $id[0] ) ) { + // Initial character must be a letter! + $id = "x$id"; + } + return $id; } /** @@ -1159,6 +1219,11 @@ class Sanitizer { # 11.2.6 'td' => array_merge( $common, $tablecell, $tablealign ), 'th' => array_merge( $common, $tablecell, $tablealign ), + + # 13.2 + # Not usually allowed, but may be used for extension-style hooks + # such as when it is rasterized + 'img' => array_merge( $common, array( 'alt' ) ), # 15.2.1 'tt' => $common, @@ -1185,6 +1250,11 @@ class Sanitizer { 'rb' => $common, 'rt' => $common, #array_merge( $common, array( 'rbspan' ) ), 'rp' => $common, + + # MathML root element, where used for extensions + # 'title' may not be 100% valid here; it's XHTML + # http://www.w3.org/TR/REC-MathML/ + 'math' => array( 'class', 'style', 'id', 'title' ), ); return $whitelist; } @@ -1274,4 +1344,4 @@ class Sanitizer { } -?> + diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index 24795ba9..11fc3deb 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -40,12 +40,10 @@ class SearchEngine { * If an exact title match can be find, or a very slightly close match, * return the title. If no match, returns NULL. * - * @static * @param string $term * @return Title - * @private */ - function getNearMatch( $searchterm ) { + public static function getNearMatch( $searchterm ) { global $wgContLang; $allSearchTerms = array($searchterm); @@ -124,8 +122,8 @@ class SearchEngine { # There may have been a funny upload, or it may be on a shared # file repository such as Wikimedia Commons. if( $title->getNamespace() == NS_IMAGE ) { - $image = new Image( $title ); - if( $image->exists() ) { + $image = wfFindFile( $title ); + if( $image ) { return $title; } } @@ -176,9 +174,8 @@ class SearchEngine { /** * Make a list of searchable namespaces and their canonical names. * @return array - * @access public */ - function searchableNamespaces() { + public static function searchableNamespaces() { global $wgContLang; $arr = array(); foreach( $wgContLang->getNamespaces() as $ns => $name ) { @@ -325,6 +322,14 @@ class SearchResultSet { function next() { return false; } + + /** + * Frees the result set, if applicable. + * @ access public + */ + function free() { + // ... + } } @@ -366,4 +371,4 @@ class SearchEngineDummy { function searchtitle() {} function searchtext() {} } -?> + diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php index 0e02a684..905075ef 100644 --- a/includes/SearchMySQL.php +++ b/includes/SearchMySQL.php @@ -200,6 +200,10 @@ class MySQLSearchResultSet extends SearchResultSet { return new SearchResult( $row ); } } + + function free() { + $this->mResultSet->free(); + } } -?> + diff --git a/includes/SearchMySQL4.php b/includes/SearchMySQL4.php index 97ce3850..6d2bbfef 100644 --- a/includes/SearchMySQL4.php +++ b/includes/SearchMySQL4.php @@ -65,4 +65,4 @@ class SearchMySQL4 extends SearchMySQL { return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) "; } } -?> + diff --git a/includes/SearchOracle.php b/includes/SearchOracle.php index c9a675e6..95c59288 100644 --- a/includes/SearchOracle.php +++ b/includes/SearchOracle.php @@ -232,4 +232,4 @@ class OracleSearchResultSet extends SearchResultSet { } } -?> + diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php index 7c3580e7..cf9e6981 100644 --- a/includes/SearchPostgres.php +++ b/includes/SearchPostgres.php @@ -64,6 +64,7 @@ class SearchPostgres extends SearchEngine { $term = preg_replace('/:/', ' ', $term); $searchstring = ''; + $m = array(); if( preg_match_all('/([-!]?)(\S+)\s*/', $term, $m, PREG_SET_ORDER ) ) { foreach( $m as $terms ) { if (strlen($terms[1])) { @@ -232,4 +233,4 @@ class PostgresSearchResultSet extends SearchResultSet { } -?> + diff --git a/includes/SearchTsearch2.php b/includes/SearchTsearch2.php index b504f034..06eaa72d 100644 --- a/includes/SearchTsearch2.php +++ b/includes/SearchTsearch2.php @@ -119,4 +119,4 @@ class SearchTsearch2 extends SearchEngine { } -?> + diff --git a/includes/SearchUpdate.php b/includes/SearchUpdate.php index 724197c1..849d6dc7 100644 --- a/includes/SearchUpdate.php +++ b/includes/SearchUpdate.php @@ -112,4 +112,4 @@ class SearchUpdateMyISAM extends SearchUpdate { # Inherits everything } -?> + diff --git a/includes/Setup.php b/includes/Setup.php index 47ba494f..66bae0a8 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -28,8 +28,8 @@ if ( !isset( $wgVersion ) ) { } // Set various default paths sensibly... -if( $wgScript === false ) $wgScript = "$wgScriptPath/index.php"; -if( $wgRedirectScript === false ) $wgRedirectScript = "$wgScriptPath/redirect.php"; +if( $wgScript === false ) $wgScript = "$wgScriptPath/index$wgScriptExtension"; +if( $wgRedirectScript === false ) $wgRedirectScript = "$wgScriptPath/redirect$wgScriptExtension"; if( $wgArticlePath === false ) { if( $wgUsePathInfo ) { @@ -54,6 +54,67 @@ if( $wgTmpDirectory === false ) $wgTmpDirectory = "{$wgUploadDirectory}/tmp"; if( $wgReadOnlyFile === false ) $wgReadOnlyFile = "{$wgUploadDirectory}/lock_yBgMBwiR"; if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirectory}/cache"; +if ( empty( $wgFileStore['deleted']['directory'] ) ) { + $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted"; +} + + +/** + * Initialise $wgLocalFileRepo from backwards-compatible settings + */ +if ( !$wgLocalFileRepo ) { + $wgLocalFileRepo = array( + 'class' => 'LocalRepo', + 'name' => 'local', + 'directory' => $wgUploadDirectory, + 'url' => $wgUploadBaseUrl ? $wgUploadBaseUrl . $wgUploadPath : $wgUploadPath, + 'hashLevels' => $wgHashedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'initialCapital' => $wgCapitalLinks, + 'deletedDir' => $wgFileStore['deleted']['directory'], + 'deletedHashLevels' => $wgFileStore['deleted']['hash'] + ); +} +/** + * Initialise shared repo from backwards-compatible settings + */ +if ( $wgUseSharedUploads ) { + if ( $wgSharedUploadDBname ) { + $wgForeignFileRepos[] = array( + 'class' => 'ForeignDBRepo', + 'name' => 'shared', + 'directory' => $wgSharedUploadDirectory, + 'url' => $wgSharedUploadPath, + 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgSharedThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'dbType' => $wgDBtype, + 'dbServer' => $wgDBserver, + 'dbUser' => $wgDBuser, + 'dbPassword' => $wgDBpassword, + 'dbName' => $wgSharedUploadDBname, + 'dbFlags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT, + 'tablePrefix' => $wgSharedUploadDBprefix, + 'hasSharedCache' => $wgCacheSharedUploads, + 'descBaseUrl' => $wgRepositoryBaseUrl, + 'fetchDescription' => $wgFetchCommonsDescriptions, + ); + } else { + $wgForeignFileRepos[] = array( + 'class' => 'FSRepo', + 'name' => 'shared', + 'directory' => $wgSharedUploadDirectory, + 'url' => $wgSharedUploadPath, + 'hashLevels' => $wgHashedSharedUploadDirectory ? 2 : 0, + 'thumbScriptUrl' => $wgSharedThumbnailScriptPath, + 'transformVia404' => !$wgGenerateThumbnailOnParse, + 'descBaseUrl' => $wgRepositoryBaseUrl, + 'fetchDescription' => $wgFetchCommonsDescriptions, + ); + } +} + require_once( "$IP/includes/AutoLoader.php" ); wfProfileIn( $fname.'-exception' ); @@ -167,6 +228,10 @@ if ( !$wgDBservers ) { $wgLoadBalancer = new StubObject( 'wgLoadBalancer', 'LoadBalancer', array( $wgDBservers, false, $wgMasterWaitTimeout, true ) ); $wgContLang = new StubContLang; + +// Now that variant lists may be available... +$wgRequest->interpolateTitle(); + $wgUser = new StubUser; $wgLang = new StubUserLang; $wgOut = new StubObject( 'wgOut', 'OutputPage' ); @@ -199,6 +264,9 @@ $wgPostCommitUpdateList = array(); if ( $wgAjaxSearch ) $wgAjaxExportList[] = 'wfSajaxSearch'; if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch'; +if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning'; +if( $wgAjaxLicensePreview ) + $wgAjaxExportList[] = 'UploadForm::ajaxGetLicensePreview'; wfSeedRandom(); @@ -232,4 +300,4 @@ $wgFullyInitialised = true; wfProfileOut( $fname.'-extensions' ); wfProfileOut( $fname ); -?> + diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php index 0968460c..353f5b3a 100644 --- a/includes/SiteConfiguration.php +++ b/includes/SiteConfiguration.php @@ -116,4 +116,4 @@ class SiteConfiguration { } } -?> + diff --git a/includes/SiteStats.php b/includes/SiteStats.php index e320a196..d7b9161a 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -5,7 +5,7 @@ */ class SiteStats { static $row, $loaded = false; - static $admins; + static $admins, $jobs; static $pageCount = array(); static function recache() { @@ -27,24 +27,26 @@ class SiteStats { $dbr = wfGetDB( DB_SLAVE ); self::$row = $dbr->selectRow( 'site_stats', '*', false, __METHOD__ ); } + + self::$loaded = true; } static function loadAndLazyInit() { wfDebug( __METHOD__ . ": reading site_stats from slave\n" ); $row = self::doLoad( wfGetDB( DB_SLAVE ) ); - - if( $row === false ) { - // Might have just been initialzed during this request? - wfDebug( __METHOD__ . ": site_stats missing on slave\n" ); + + if( !self::isSane( $row ) ) { + // Might have just been initialized during this request? Underflow? + wfDebug( __METHOD__ . ": site_stats damaged or missing on slave\n" ); $row = self::doLoad( wfGetDB( DB_MASTER ) ); } - if( $row === false ) { + if( !self::isSane( $row ) ) { // Normally the site_stats table is initialized at install time. - // Some manual construction scenarios may leave the table empty, - // however, for instance when importing from a dump into a clean - // schema with mwdumper. - wfDebug( __METHOD__ . ": initializing empty site_stats\n" ); + // Some manual construction scenarios may leave the table empty or + // broken, however, for instance when importing from a dump into a + // clean schema with mwdumper. + wfDebug( __METHOD__ . ": initializing damaged or missing site_stats\n" ); global $IP; require_once "$IP/maintenance/initStats.inc"; @@ -56,8 +58,8 @@ class SiteStats { $row = self::doLoad( wfGetDB( DB_MASTER ) ); } - if( $row === false ) { - wfDebug( __METHOD__ . ": init of site_stats failed o_O\n" ); + if( !self::isSane( $row ) ) { + wfDebug( __METHOD__ . ": site_stats persistently nonsensical o_O\n" ); } return $row; } @@ -104,6 +106,18 @@ class SiteStats { return self::$admins; } + static function jobs() { + if ( !isset( self::$jobs ) ) { + $dbr = wfGetDB( DB_SLAVE ); + self::$jobs = $dbr->estimateRowCount('job'); + /* Zero rows still do single row read for row that doesn't exist, but people are annoyed by that */ + if (self::$jobs == 1) { + self::$jobs = 0; + } + } + return self::$jobs; + } + static function pagesInNs( $ns ) { wfProfileIn( __METHOD__ ); if( !isset( self::$pageCount[$ns] ) ) { @@ -114,6 +128,28 @@ class SiteStats { return $pageCount[$ns]; } + /** Is the provided row of site stats sane, or should it be regenerated? */ + private static function isSane( $row ) { + if( + $row === false + or $row->ss_total_pages < $row->ss_good_articles + or $row->ss_total_edits < $row->ss_total_pages + or $row->ss_users < $row->ss_admins + ) { + return false; + } + // Now check for underflow/overflow + foreach( array( 'total_views', 'total_edits', 'good_articles', + 'total_pages', 'users', 'admins', 'images' ) as $member ) { + if( + $row->{"ss_$member"} > 2000000000 + or $row->{"ss_$member"} < 0 + ) { + return false; + } + } + return true; + } } @@ -200,4 +236,4 @@ class SiteStatsUpdate { */ } } -?> + diff --git a/includes/Skin.php b/includes/Skin.php index 0ca95f7e..f9e17057 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -22,6 +22,7 @@ class Skin extends Linker { var $rcMoveIndex; var $mWatchLinkNum = 0; // Appended to end of watch link id's /**#@-*/ + protected $mRevisionId; // The revision ID we're looking at, null if not applicable. protected $skinname = 'standard' ; /** Constructor, call parent constructor */ @@ -292,19 +293,21 @@ class Skin extends Linker { * The odd calling convention is for backwards compatibility */ static function makeGlobalVariablesScript( $data ) { - global $wgStylePath, $wgUser; + global $wgScript, $wgStylePath, $wgUser; global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang; global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle; global $wgBreakFrames, $wgRequest; + global $wgUseAjax, $wgAjaxWatch; $ns = $wgTitle->getNamespace(); $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText(); - + $vars = array( 'skin' => $data['skinname'], 'stylepath' => $wgStylePath, 'wgArticlePath' => $wgArticlePath, 'wgScriptPath' => $wgScriptPath, + 'wgScript' => $wgScript, 'wgServer' => $wgServer, 'wgCanonicalNamespace' => $nsname, 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBKey() ), @@ -312,6 +315,8 @@ class Skin extends Linker { 'wgPageName' => $wgTitle->getPrefixedDBKey(), 'wgTitle' => $wgTitle->getText(), 'wgAction' => $wgRequest->getText( 'action', 'view' ), + 'wgRestrictionEdit' => $wgTitle->getRestrictions( 'edit' ), + 'wgRestrictionMove' => $wgTitle->getRestrictions( 'move' ), 'wgArticleId' => $wgTitle->getArticleId(), 'wgIsArticle' => $wgOut->isArticle(), 'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(), @@ -330,24 +335,33 @@ class Skin extends Linker { $vars['wgLivepreviewMessageError'] = wfMsg( 'livepreview-error' ); } + if($wgUseAjax && $wgAjaxWatch && $wgUser->isLoggedIn() ) { + $msgs = (object)array(); + foreach ( array( 'watch', 'unwatch', 'watching', 'unwatching' ) as $msgName ) { + $msgs->{$msgName . 'Msg'} = wfMsg( $msgName ); + } + $vars['wgAjaxWatch'] = $msgs; + } + return self::makeVariablesScript( $vars ); } - function getHeadScripts() { - global $wgStylePath, $wgUser, $wgAllowUserJs, $wgJsMimeType, $wgStyleVersion; + function getHeadScripts( $allowUserJs ) { + global $wgStylePath, $wgUser, $wgJsMimeType, $wgStyleVersion; $r = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) ); $r .= "\n"; global $wgUseSiteJs; if ($wgUseSiteJs) { - if ($wgUser->isLoggedIn()) { - $r .= "\n"; - } else { - $r .= "\n"; - } - } - if( $wgAllowUserJs && $wgUser->isLoggedIn() ) { + $jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : ''; + $r .= "\n"; + } + if( $allowUserJs && $wgUser->isLoggedIn() ) { $userpage = $wgUser->getUserPage(); $userjs = htmlspecialchars( self::makeUrl( $userpage->getPrefixedText().'/'.$this->getSkinName().'.js', @@ -385,7 +399,8 @@ class Skin extends Linker { function getUserStylesheet() { global $wgStylePath, $wgRequest, $wgContLang, $wgSquidMaxage, $wgStyleVersion; $sheet = $this->getStylesheet(); - $s = "@import \"$wgStylePath/common/common.css?$wgStyleVersion\";\n"; + $s = "@import \"$wgStylePath/common/shared.css?$wgStyleVersion\";\n"; + $s .= "@import \"$wgStylePath/common/oldshared.css?$wgStyleVersion\";\n"; $s .= "@import \"$wgStylePath/$sheet?$wgStyleVersion\";\n"; if($wgContLang->isRTL()) $s .= "@import \"$wgStylePath/common/common_rtl.css?$wgStyleVersion\";\n"; @@ -398,36 +413,28 @@ class Skin extends Linker { } /** - * This returns MediaWiki:Common.js. For some bizarre reason, it does - * *not* return any custom user JS from user subpages. Huh? + * This returns MediaWiki:Common.js, and derived classes may add other JS. + * Despite its name, it does *not* return any custom user JS from user + * subpages. The returned script is sitewide and publicly cacheable and + * therefore must not include anything that varies according to user, + * interface language, etc. (although it may vary by skin). See + * makeGlobalVariablesScript for things that can vary per page view and are + * not cacheable. * - * @return string + * @return string Raw JavaScript to be returned */ - function getUserJs() { + public function getUserJs() { wfProfileIn( __METHOD__ ); global $wgStylePath; $s = "/* generated javascript */\n"; - $s .= "var skin = '{$this->skinname}';\nvar stylepath = '{$wgStylePath}';"; + $s .= "var skin = '" . Xml::escapeJsString( $this->getSkinName() ) . "';\n"; + $s .= "var stylepath = '" . Xml::escapeJsString( $wgStylePath ) . "';"; $s .= "\n\n/* MediaWiki:Common.js */\n"; $commonJs = wfMsgForContent('common.js'); if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) { $s .= $commonJs; } - - global $wgUseAjax, $wgAjaxWatch; - if($wgUseAjax && $wgAjaxWatch) { - $s .= " - -/* AJAX (un)watch (see /skins/common/ajaxwatch.js) */ -var wgAjaxWatch = { - watchMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'watch', array() ) )."', - unwatchMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'unwatch', array() ) )."', - watchingMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'watching', array() ) )."', - unwatchingMsg: '". str_replace( array("'", "\n"), array("\\'", ' '), wfMsgExt( 'unwatching', array() ) )."' -};"; - } - wfProfileOut( __METHOD__ ); return $s; } @@ -701,7 +708,9 @@ END; */ function bottomScripts() { global $wgJsMimeType; - return "\n\t\t\n"; + $bottomScriptText = "\n\t\t\n"; + wfRunHooks( 'SkinAfterBottomScripts', array( $this, &$bottomScriptText ) ); + return $bottomScriptText; } /** @return string Retrievied from HTML text */ @@ -739,8 +748,8 @@ END; if ( $wgOut->isArticleRelated() ) { if ( $wgTitle->getNamespace() == NS_IMAGE ) { $name = $wgTitle->getDBkey(); - $image = new Image( $wgTitle ); - if( $image->exists() ) { + $image = wfFindFile( $wgTitle ); + if( $image ) { $link = htmlspecialchars( $image->getURL() ); $style = $this->getInternalLinkAttributes( $link, $name ); $s .= " | {$name}"; @@ -1427,29 +1436,6 @@ END; return $s; } - function dateLink() { - $t1 = Title::newFromText( gmdate( 'F j' ) ); - $t2 = Title::newFromText( gmdate( 'Y' ) ); - - $id = $t1->getArticleID(); - - if ( 0 == $id ) { - $s = $this->makeBrokenLink( $t1->getText() ); - } else { - $s = $this->makeKnownLink( $t1->getText() ); - } - $s .= ', '; - - $id = $t2->getArticleID(); - - if ( 0 == $id ) { - $s .= $this->makeBrokenLink( $t2->getText() ); - } else { - $s .= $this->makeKnownLink( $t2->getText() ); - } - return $s; - } - function talkLink() { global $wgTitle; @@ -1668,5 +1654,5 @@ END; wfProfileOut( $fname ); return $bar; } -} -?> + +} \ No newline at end of file diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index cddd2195..6ce40606 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -179,7 +179,7 @@ class SkinTemplate extends Skin { $this->usercss = $this->userjs = $this->userjsprev = false; $this->setupUserCss(); - $this->setupUserJs(); + $this->setupUserJs( $out->isUserJsAllowed() ); $this->titletxt = $this->mTitle->getPrefixedText(); wfProfileOut( "$fname-stuff" ); @@ -285,11 +285,11 @@ class SkinTemplate extends Skin { $tpl->setRef( 'userjsprev', $this->userjsprev); global $wgUseSiteJs; if ($wgUseSiteJs) { - if($this->loggedin) { - $tpl->set( 'jsvarurl', self::makeUrl('-','action=raw&smaxage=0&gen=js') ); - } else { - $tpl->set( 'jsvarurl', self::makeUrl('-','action=raw&gen=js') ); - } + $jsCache = $this->loggedin ? '&smaxage=0' : ''; + $tpl->set( 'jsvarurl', + self::makeUrl('-', + "action=raw$jsCache&gen=js&useskin=" . + urlencode( $this->getSkinName() ) ) ); } else { $tpl->set('jsvarurl', false); } @@ -440,7 +440,8 @@ class SkinTemplate extends Skin { // XXX: attach this from javascript, same with section editing if($this->iseditable && $wgUser->getOption("editondblclick") ) { - $tpl->set('body_ondblclick', 'document.location = "' .$content_actions['edit']['href'] .'";'); + $encEditUrl = wfEscapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) ); + $tpl->set('body_ondblclick', 'document.location = "' . $encEditUrl . '";'); } else { $tpl->set('body_ondblclick', false); } @@ -591,7 +592,7 @@ class SkinTemplate extends Skin { if( $selected ) { $classes[] = 'selected'; } - if( $checkEdit && $title->getArticleId() == 0 ) { + if( $checkEdit && !$title->isAlwaysKnown() && $title->getArticleId() == 0 ) { $classes[] = 'new'; $query = 'action=edit'; } @@ -822,7 +823,6 @@ class SkinTemplate extends Skin { global $wgEnableUploads, $wgUploadNavigationUrl; $action = $wgRequest->getText( 'action' ); - $oldid = $wgRequest->getVal( 'oldid' ); $nav_urls = array(); $nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() ); @@ -852,21 +852,16 @@ class SkinTemplate extends Skin { ); // Also add a "permalink" while we're at it - if ( (int)$oldid ) { + if ( $this->mRevisionId ) { $nav_urls['permalink'] = array( 'text' => wfMsg( 'permalink' ), - 'href' => '' + 'href' => $wgTitle->getLocalURL( "oldid=$this->mRevisionId" ) ); - } else { - $revid = $wgArticle ? $wgArticle->getLatest() : 0; - if ( !( $revid == 0 ) ) - $nav_urls['permalink'] = array( - 'text' => wfMsg( 'permalink' ), - 'href' => $wgTitle->getLocalURL( "oldid=$revid" ) - ); } - - wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', array( &$this, &$nav_urls, &$oldid, &$revid ) ); + + // Copy in case this undocumented, shady hook tries to mess with internals + $revid = $this->mRevisionId; + wfRunHooks( 'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink', array( &$this, &$nav_urls, &$revid, &$revid ) ); } if( $this->mTitle->getNamespace() != NS_SPECIAL ) { @@ -896,10 +891,19 @@ class SkinTemplate extends Skin { $ip = false; } - if($id || $ip) { # both anons and non-anons have contri list + if($id || $ip) { # both anons and non-anons have contribs list $nav_urls['contributions'] = array( 'href' => self::makeSpecialUrlSubpage( 'Contributions', $this->mTitle->getText() ) ); + + if( $id ) { + $logPage = SpecialPage::getTitleFor( 'Log' ); + $nav_urls['log'] = array( 'href' => $logPage->getLocalUrl( 'user=' + . $this->mTitle->getPartialUrl() ) ); + } else { + $nav_urls['log'] = false; + } + if ( $wgUser->isAllowed( 'block' ) ) { $nav_urls['blockip'] = array( 'href' => self::makeSpecialUrlSubpage( 'Blockip', $this->mTitle->getText() ) @@ -909,6 +913,7 @@ class SkinTemplate extends Skin { } } else { $nav_urls['contributions'] = false; + $nav_urls['log'] = false; $nav_urls['blockip'] = false; } $nav_urls['emailuser'] = false; @@ -974,9 +979,12 @@ class SkinTemplate extends Skin { # If we use the site's dynamic CSS, throw that in, too if ( $wgUseSiteCss ) { $query = "usemsgcache=yes&action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; + $skinquery = ''; + if (($us = $wgRequest->getVal('useskin', '')) !== '') + $skinquery = "&useskin=$us"; $sitecss .= '@import "' . self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI) . '";' . "\n"; $sitecss .= '@import "' . self::makeNSUrl( ucfirst( $this->skinname ) . '.css', $query, NS_MEDIAWIKI ) . '";' . "\n"; - $sitecss .= '@import "' . self::makeUrl( '-', 'action=raw&gen=css' . $siteargs ) . '";' . "\n"; + $sitecss .= '@import "' . self::makeUrl( '-', "action=raw&gen=css$siteargs$skinquery" ) . '";' . "\n"; } # If we use any dynamic CSS, make a little CDATA block out of it. @@ -990,14 +998,14 @@ class SkinTemplate extends Skin { /** * @private */ - function setupUserJs() { + function setupUserJs( $allowUserJs ) { $fname = 'SkinTemplate::setupUserJs'; wfProfileIn( $fname ); - global $wgRequest, $wgAllowUserJs, $wgJsMimeType; + global $wgRequest, $wgJsMimeType; $action = $wgRequest->getText('action'); - if( $wgAllowUserJs && $this->loggedin ) { + if( $allowUserJs && $this->loggedin ) { if( $this->mTitle->isJsSubpage() and $this->userCanPreview( $action ) ) { # XXX: additional security check/prompt? $this->userjsprev = '/**/ ' . $wgRequest->getText('wpTextbox1') . ' /**/'; @@ -1177,4 +1185,4 @@ class QuickTemplate { return ($msg != '-') && ($msg != ''); # ???? } } -?> + diff --git a/includes/SpecialAllmessages.php b/includes/SpecialAllmessages.php index 0862cd17..4ba01e29 100644 --- a/includes/SpecialAllmessages.php +++ b/includes/SpecialAllmessages.php @@ -25,7 +25,8 @@ function wfSpecialAllmessages() { $navText = wfMsg( 'allmessagestext' ); # Make sure all extension messages are available - MessageCache::loadAllMessages(); + + $wgMessageCache->loadAllMessages(); $sortedArray = array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ); ksort( $sortedArray ); @@ -196,4 +197,4 @@ $mw return $txt; } -?> + diff --git a/includes/SpecialAllpages.php b/includes/SpecialAllpages.php index 03e164bd..07ff120b 100644 --- a/includes/SpecialAllpages.php +++ b/includes/SpecialAllpages.php @@ -19,10 +19,7 @@ function wfSpecialAllpages( $par=NULL, $specialPage ) { $indexPage = new SpecialAllpages(); - if( !in_array($namespace, array_keys($namespaces)) ) - $namespace = 0; - - $wgOut->setPagetitle( $namespace > 0 ? + $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : wfMsg( 'allarticles' ) ); @@ -53,41 +50,44 @@ class SpecialAllpages { * @param string $from Article name we are starting listing at. */ function namespaceForm ( $namespace = NS_MAIN, $from = '' ) { - global $wgScript; + global $wgScript, $wgContLang; $t = SpecialPage::getTitleFor( $this->name ); - - $namespaceselect = HTMLnamespaceselector($namespace, null); - - $frombox = "'; - $submitbutton = ''; - - $out = "
    "; - $out .= ''; - $out .= " -
    ' . $this->buildCascadeInput() . "
    " . $this->buildReasonInput() . "
    " . $this->buildWatchInput() . "
    " . $this->buildSubmit() . "
    "; - $ci .= wfElement( 'label', array ( - 'id' => "$id-label", - 'for' => $id ), - wfMsg( 'protectexpiry' ) ); - $ci .= " "; - $ci .= wfElement( 'input', array( - 'size' => 60, - 'name' => $id, - 'id' => $id, - 'value' => $this->mExpiry ) + $this->disabledAttrib ); - $ci .= "
    ' . Xml::input( 'mwProtect-expiry', 60, $this->mExpiry, $attribs ) . '
    - - - - - - - - -
    " . wfMsgHtml($this->nsfromMsg) . "
    - $namespaceselect $submitbutton -
    -"; - $out .= ''; - return $out; + $align = $wgContLang->isRtl() ? 'left' : 'right'; + + $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) ); + $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $out .= Xml::hidden( 'title', $t->getPrefixedText() ); + $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) ); + $out .= " + " . + Xml::label( wfMsg( $this->nsfromMsg ), 'nsfrom' ) . + " + " . + Xml::input( 'from', 20, htmlspecialchars ( $from ), array( 'id' => 'nsfrom' ) ) . + " + + + " . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + " + " . + Xml::namespaceSelector( $namespace, null ) . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + " + "; + $out .= Xml::closeElement( 'table' ); + $out .= Xml::closeElement( 'form' ); + $out .= Xml::closeElement( 'div' ); + return $out; } /** * @param integer $namespace (default NS_MAIN) */ function showToplevel ( $namespace = NS_MAIN, $including = false ) { - global $wgOut; + global $wgOut, $wgContLang; $fname = "indexShowToplevel"; + $align = $wgContLang->isRtl() ? 'left' : 'right'; # TODO: Either make this *much* faster or cache the title index points # in the querycache table. @@ -101,7 +101,11 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { $lines = $wgMemc->get( $key ); if( !is_array( $lines ) ) { - $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, array( 'LIMIT' => 1 ) ); + $options = array( 'LIMIT' => 1 ); + if ( ! $dbr->implicitOrderby() ) { + $options['ORDER BY'] = 'page_title'; + } + $firstTitle = $dbr->selectField( 'page', 'page_title', $where, $fname, $options ); $lastTitle = $firstTitle; # This array is going to hold the page_titles in order. @@ -170,8 +174,8 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { $morelinks = ''; if ( $morelinks != '' ) { $out2 = ''; - $out2 .= '
    ' . $nsForm; - $out2 .= ''; + $out2 .= '
    ' . $nsForm; + $out2 .= ''; $out2 .= $morelinks . '

    '; } else { $out2 = $nsForm . '
    '; @@ -187,6 +191,8 @@ function showToplevel ( $namespace = NS_MAIN, $including = false ) { * @param integer $namespace (Default NS_MAIN) */ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { + global $wgContLang; + $align = $wgContLang->isRtl() ? 'left' : 'right'; $inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) ); $outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) ); $queryparams = ($namespace ? "namespace=$namespace" : ''); @@ -196,9 +202,9 @@ function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) { $out = wfMsgHtml( 'alphaindexline', "
    $inpointf", - "$outpointf" + "$outpointf" ); - return ''.$out.''; + return ''.$out.''; } /** @@ -209,14 +215,20 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { global $wgOut, $wgUser, $wgContLang; $fname = 'indexShowChunk'; - $sk = $wgUser->getSkin(); $fromList = $this->getNamespaceKeyAndText($namespace, $from); + $namespaces = $wgContLang->getNamespaces(); + $align = $wgContLang->isRtl() ? 'left' : 'right'; + $n = 0; - + if ( !$fromList ) { $out = wfMsgWikiHtml( 'allpagesbadtitle' ); + } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { + // Show errormessage and reset to NS_MAIN + $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace ); + $namespace = NS_MAIN; } else { list( $namespace, $fromKey, $from ) = $fromList; @@ -285,8 +297,11 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { } else { # The previous chunk is not complete, need to link to the very first title # available in the database - $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, array( 'LIMIT' => 1) ); - + $options = array( 'LIMIT' => 1 ); + if ( ! $dbr->implicitOrderby() ) { + $options['ORDER BY'] = 'page_title'; + } + $reallyFirstPage_title = $dbr->selectField( 'page', 'page_title', array( 'page_namespace' => $namespace ), $fname, $options ); # Show the previous link if it s not the current requested chunk if( $from != $reallyFirstPage_title ) { $prevTitle = Title::makeTitle( $namespace, $reallyFirstPage_title ); @@ -298,8 +313,8 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { $nsForm = $this->namespaceForm ( $namespace, $from ); $out2 = ''; - $out2 .= ' + + + + + + + + + + + " . + Xml::closeElement( 'table' ) . + Xml::hidden( 'wpEditToken', $token ) . + Xml::closeElement( 'form' ) . "\n" + ); } @@ -200,46 +224,34 @@ class IPUnblockForm { } } - # TODO: difference message between - # a) an real empty list and - # b) requested ip/username not on list $pager = new IPBlocklistPager( $this, $conds ); if ( $pager->getNumRows() ) { - $s = $this->searchForm() . - $pager->getNavigationBar(); - $s .= "
      " . - $pager->getBody() . - "
    "; - $s .= $pager->getNavigationBar(); + $wgOut->addHTML( + $this->searchForm() . + $pager->getNavigationBar() . + Xml::tags( 'ul', null, $pager->getBody() ) . + $pager->getNavigationBar() + ); + } elseif ( $this->ip != '') { + $wgOut->addHTML( $this->searchForm() ); + $wgOut->addWikiText( wfMsg( 'ipblocklist-no-results' ) ); } else { - $s = $this->searchForm() . - '

    ' . wfMsgHTML( 'ipblocklistempty' ) . '

    '; + $wgOut->addWikiText( wfMsg( 'ipblocklist-empty' ) ); } - $wgOut->addHTML( $s ); } function searchForm() { global $wgTitle, $wgScript, $wgRequest; return - wfElement( 'form', array( - 'action' => $wgScript ), - null ) . - wfHidden( 'title', $wgTitle->getPrefixedDbKey() ) . - wfElement( 'input', array( - 'type' => 'hidden', - 'name' => 'action', - 'value' => 'search' ) ). - wfElement( 'input', array( - 'type' => 'hidden', - 'name' => 'limit', - 'value' => $wgRequest->getText( 'limit' ) ) ) . - wfElement( 'input', array( - 'name' => 'ip', - 'value' => $this->ip ) ) . - wfElement( 'input', array( - 'type' => 'submit', - 'value' => wfMsg( 'ipblocklist-submit' ) ) ) . - ''; + Xml::tags( 'form', array( 'action' => $wgScript ), + Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) . + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) . + Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) . + ' ' . + Xml::submitButton( wfMsg( 'ipblocklist-submit' ) ) . + Xml::closeElement( 'fieldset' ) + ); } /** @@ -257,7 +269,7 @@ class IPUnblockForm { if( is_null( $msg ) ) { $msg = array(); $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink', - 'anononlyblock', 'createaccountblock', 'noautoblockblock' ); + 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock' ); foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } @@ -275,8 +287,8 @@ class IPUnblockForm { if( $block->mAuto ) { $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy } else { - $target = $sk->makeLinkObj( Title::makeTitle( NS_USER, $block->mAddress ), $block->mAddress ); - $target .= ' (' . $sk->makeKnownLinkObj( SpecialPage::getSafeTitleFor( 'Contributions', $block->mAddress ), $msg['contribslink'] ) . ')'; + $target = $sk->userLink( $block->mUser, $block->mAddress ) + . $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK ); } $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); @@ -298,6 +310,10 @@ class IPUnblockForm { $properties[] = $msg['noautoblockblock']; } + if ( $block->mBlockEmail && $block->mUser ) { + $properties[] = $msg['emailblock']; + } + $properties = implode( ', ', $properties ); $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) ); @@ -383,4 +399,4 @@ class IPBlocklistPager extends ReverseChronologicalPager { } } -?> + diff --git a/includes/SpecialListredirects.php b/includes/SpecialListredirects.php index 09dc2b39..581ea55b 100644 --- a/includes/SpecialListredirects.php +++ b/includes/SpecialListredirects.php @@ -62,4 +62,4 @@ function wfSpecialListredirects() { $lrp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialListusers.php b/includes/SpecialListusers.php index 42498430..460d4259 100644 --- a/includes/SpecialListusers.php +++ b/includes/SpecialListusers.php @@ -103,7 +103,6 @@ class UsersPager extends AlphabeticPager { $this->doQuery(); } $batch = new LinkBatch; - $db = $this->mDb; $this->mResult->rewind(); @@ -116,35 +115,30 @@ class UsersPager extends AlphabeticPager { } function getPageHeader( ) { - global $wgRequest; + global $wgScript, $wgRequest; $self = $this->getTitle(); # Form tag - $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $self->getLocalUrl() ) ) . + $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) . '
    ' . Xml::element( 'legend', array(), wfMsg( 'listusers' ) ); + $out .= Xml::hidden( 'title', $self->getPrefixedDbKey() ); # Username field $out .= Xml::label( wfMsg( 'listusersfrom' ), 'offset' ) . ' ' . Xml::input( 'username', 20, $this->requestedUser, array( 'id' => 'offset' ) ) . ' '; - if( $this->mLimit ) - $out .= Xml::hidden( 'limit', $this->mLimit ); - # Group drop-down list $out .= Xml::label( wfMsg( 'group' ), 'group' ) . ' ' . Xml::openElement('select', array( 'name' => 'group', 'id' => 'group' ) ) . - Xml::option( wfMsg( 'group-all' ), '' ); # Item for "all groups" - - $groups = User::getAllGroups(); - foreach( $groups as $group ) { - $attribs = array( 'value' => $group ); - $attribs['selected'] = ( $group == $this->requestedGroup ) ? 'selected' : ''; - $out .= Xml::option( User::getGroupName( $group ), $attribs['value'], $attribs['selected'] ); - } + Xml::option( wfMsg( 'group-all' ), '' ); + foreach( User::getAllGroups() as $group ) + $out .= Xml::option( User::getGroupName( $group ), $group, $group == $this->requestedGroup ); $out .= Xml::closeElement( 'select' ) . ' '; # Submit button and form bottom + if( $this->mLimit ) + $out .= Xml::hidden( 'limit', $this->mLimit ); $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . '
    ' . Xml::closeElement( 'form' ); @@ -204,10 +198,6 @@ class UsersPager extends AlphabeticPager { function wfSpecialListusers( $par = null ) { global $wgRequest, $wgOut; - list( $limit, $offset ) = wfCheckLimits(); - - $groupTarget = isset($par) ? $par : $wgRequest->getVal( 'group' ); - $up = new UsersPager($par); # getBody() first to check, if empty @@ -224,4 +214,4 @@ function wfSpecialListusers( $par = null ) { $wgOut->addHTML( $s ); } -?> + diff --git a/includes/SpecialLockdb.php b/includes/SpecialLockdb.php index db4006f5..e57717e2 100644 --- a/includes/SpecialLockdb.php +++ b/includes/SpecialLockdb.php @@ -131,4 +131,4 @@ END } -?> + diff --git a/includes/SpecialLog.php b/includes/SpecialLog.php index 3c9d0960..f0794eb5 100644 --- a/includes/SpecialLog.php +++ b/includes/SpecialLog.php @@ -74,7 +74,8 @@ class LogReader { // XXX This all needs to use Pager, ugly hack for now. global $wgMiserMode; - if ($wgMiserMode && ($this->offset >10000)) $this->offset=10000; + if( $wgMiserMode ) + $this->offset = min( $this->offset, 10000 ); } /** @@ -123,9 +124,10 @@ class LogReader { function limitTitle( $page , $pattern ) { global $wgMiserMode; $title = Title::newFromText( $page ); - if( empty( $page ) || is_null( $title ) ) { + + if( strlen( $page ) == 0 || !$title instanceof Title ) return false; - } + $this->title =& $title; $this->pattern = $pattern; $ns = $title->getNamespace(); @@ -215,6 +217,23 @@ class LogReader { return $this->title->getPrefixedText(); } } + + /** + * Is there at least one row? + * + * @return bool + */ + public function hasRows() { + # Little hack... + $limit = $this->limit; + $this->limit = 1; + $res = $this->db->query( $this->getQuery() ); + $this->limit = $limit; + $ret = $this->db->numRows( $res ) > 0; + $this->db->freeResult( $res ); + return $ret; + } + } /** @@ -222,19 +241,25 @@ class LogReader { * @addtogroup SpecialPage */ class LogViewer { + const NO_ACTION_LINK = 1; + /** * @var LogReader $reader */ var $reader; var $numResults = 0; + var $flags = 0; /** * @param LogReader &$reader where to get our data from + * @param integer $flags Bitwise combination of flags: + * self::NO_ACTION_LINK Don't show restore/unblock/block links */ - function LogViewer( &$reader ) { + function LogViewer( &$reader, $flags = 0 ) { global $wgUser; $this->skin = $wgUser->getSkin(); $this->reader =& $reader; + $this->flags = $flags; } /** @@ -325,7 +350,7 @@ class LogViewer { * @private */ function logLine( $s ) { - global $wgLang, $wgUser;; + global $wgLang, $wgUser, $wgContLang; $skin = $wgUser->getSkin(); $title = Title::makeTitle( $s->log_namespace, $s->log_title ); $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $s->log_timestamp), true ); @@ -340,42 +365,47 @@ class LogViewer { } $userLink = $this->skin->userLink( $s->log_user, $s->user_name ) . $this->skin->userToolLinksRedContribs( $s->log_user, $s->user_name ); - $comment = $this->skin->commentBlock( $s->log_comment ); + $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment ); $paramArray = LogPage::extractParams( $s->log_params ); $revert = ''; // show revertmove link - if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { - $destTitle = Title::newFromText( $paramArray[0] ); - if ( $destTitle ) { - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), - wfMsg( 'revertmove' ), - 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . - '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . - '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . - '&wpMovetalk=0' ) . ')'; + if ( !( $this->flags & self::NO_ACTION_LINK ) ) { + if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) { + $destTitle = Title::newFromText( $paramArray[0] ); + if ( $destTitle ) { + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ), + wfMsg( 'revertmove' ), + 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) . + '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) . + '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) . + '&wpMovetalk=0' ) . ')'; + } + // show undelete link + } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) { + $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), + wfMsg( 'undeletebtn' ) , + 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; + + // show unblock link + } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) { + $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), + wfMsg( 'unblocklink' ), + 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')'; + // show change protection link + } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) { + $revert = '(' . $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')'; + // show user tool links for self created users + // TODO: The extension should be handling this, get it out of core! + } elseif ( $s->log_action == 'create2' ) { + if( isset( $paramArray[0] ) ) { + $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true ); + } else { + # Fall back to a blue contributions link + $revert = $this->skin->userToolLinks( 1, $s->log_title ); + } + # Suppress $comment from old entries, not needed and can contain incorrect links + $comment = ''; } - // show undelete link - } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) { - $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), - wfMsg( 'undeletebtn' ) , - 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')'; - - // show unblock link - } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) { - $revert = '(' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ), - wfMsg( 'unblocklink' ), - 'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')'; - // show change protection link - } elseif ( $s->log_action == 'protect' && $wgUser->isAllowed( 'protect' ) ) { - $revert = '(' . $skin->makeKnownLink( $title->getPrefixedDBkey() , - wfMsg( 'protect_change' ), - 'action=unprotect' ) . ')'; - // show user tool links for self created users - } elseif ( $s->log_action == 'create2' ) { - $revert = $this->skin->userToolLinksRedContribs( $s->log_user, $s->log_title ); - // do not show $comment for self created accounts. It includes wrong user tool links: - // 'blockip' for users w/o block allowance and broken links for very long usernames (bug 4756) - $comment = ''; } $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true, true ); @@ -497,4 +527,4 @@ class LogViewer { } -?> + diff --git a/includes/SpecialLonelypages.php b/includes/SpecialLonelypages.php index 430af7a7..e652f9d4 100644 --- a/includes/SpecialLonelypages.php +++ b/includes/SpecialLonelypages.php @@ -15,7 +15,7 @@ class LonelyPagesPage extends PageQueryPage { return "Lonelypages"; } function getPageHeader() { - return '

    ' . wfMsg('lonelypagestext') . '

    '; + return wfMsgExt( 'lonelypagestext', array( 'parse' ) ); } function sortDescending() { @@ -57,4 +57,4 @@ function wfSpecialLonelypages() { return $lpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialLongpages.php b/includes/SpecialLongpages.php index 40659889..a8a1e199 100644 --- a/includes/SpecialLongpages.php +++ b/includes/SpecialLongpages.php @@ -30,4 +30,4 @@ function wfSpecialLongpages() { $lpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialMIMEsearch.php b/includes/SpecialMIMEsearch.php index d50efc02..c89c1af6 100644 --- a/includes/SpecialMIMEsearch.php +++ b/includes/SpecialMIMEsearch.php @@ -66,7 +66,7 @@ class MIMEsearchPage extends QueryPage { $text = $wgContLang->convert( $nt->getText() ); $plink = $skin->makeLink( $nt->getPrefixedText(), $text ); - $download = $skin->makeMediaLink( $nt->getText(), 'fuck me!', wfMsgHtml( 'download' ) ); + $download = $skin->makeMediaLinkObj( $nt, wfMsgHtml( 'download' ) ); $bytes = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), $wgLang->formatNum( $result->img_size ) ); $dimensions = wfMsgHtml( 'widthheight', $wgLang->formatNum( $result->img_width ), @@ -87,33 +87,16 @@ function wfSpecialMIMEsearch( $par = null ) { $mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' ); $wgOut->addHTML( - wfElement( 'form', + Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->escapeLocalUrl() - ), - null + ) ) . - wfOpenElement( 'label' ) . - wfMsgHtml( 'mimetype' ) . - wfElement( 'input', array( - 'type' => 'text', - 'size' => 20, - 'name' => 'mime', - 'value' => $mime - ), - '' - ) . - ' ' . - wfElement( 'input', array( - 'type' => 'submit', - 'value' => wfMsg( 'ilsubmit' ) - ), - '' - ) . - wfCloseElement( 'label' ) . - wfCloseElement( 'form' ) + Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . + Xml::submitButton( wfMsg( 'ilsubmit' ) ) . + Xml::closeElement( 'form' ) ); list( $major, $minor ) = wfSpecialMIMEsearchParse( $mime ); @@ -155,4 +138,4 @@ function wfSpecialMIMEsearchValidType( $type ) { return in_array( $type, $types ); } -?> + diff --git a/includes/SpecialMostcategories.php b/includes/SpecialMostcategories.php index df2b9adf..589b96ee 100644 --- a/includes/SpecialMostcategories.php +++ b/includes/SpecialMostcategories.php @@ -38,6 +38,7 @@ class MostcategoriesPage extends QueryPage { function formatResult( $skin, $result ) { global $wgLang; $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if ( !$title instanceof Title ) { throw new MWException('Invalid title in database'); } $count = wfMsgExt( 'ncategories', array( 'parsemag', 'escape' ), $wgLang->formatNum( $result->value ) ); $link = $skin->makeKnownLinkObj( $title, $title->getText() ); return wfSpecialList( $link, $count ); @@ -55,4 +56,4 @@ function wfSpecialMostcategories() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialMostimages.php b/includes/SpecialMostimages.php index 9d16f389..beb42fc1 100644 --- a/includes/SpecialMostimages.php +++ b/includes/SpecialMostimages.php @@ -52,4 +52,4 @@ function wfSpecialMostimages() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialMostlinked.php b/includes/SpecialMostlinked.php index ab089cf8..b4de0a0e 100644 --- a/includes/SpecialMostlinked.php +++ b/includes/SpecialMostlinked.php @@ -90,4 +90,4 @@ function wfSpecialMostlinked() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialMostlinkedcategories.php b/includes/SpecialMostlinkedcategories.php index 725e5b39..d0a99b3b 100644 --- a/includes/SpecialMostlinkedcategories.php +++ b/includes/SpecialMostlinkedcategories.php @@ -72,4 +72,4 @@ function wfSpecialMostlinkedCategories() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialMostlinkedtemplates.php b/includes/SpecialMostlinkedtemplates.php new file mode 100644 index 00000000..e7e7afcc --- /dev/null +++ b/includes/SpecialMostlinkedtemplates.php @@ -0,0 +1,131 @@ + + */ +class SpecialMostlinkedtemplates extends QueryPage { + + /** + * Name of the report + * + * @return string + */ + public function getName() { + return 'Mostlinkedtemplates'; + } + + /** + * Is this report expensive, i.e should it be cached? + * + * @return bool + */ + public function isExpensive() { + return true; + } + + /** + * Is there a feed available? + * + * @return bool + */ + public function isSyndicated() { + return false; + } + + /** + * Sort the results in descending order? + * + * @return bool + */ + public function sortDescending() { + return true; + } + + /** + * Generate SQL for the report + * + * @return string + */ + public function getSql() { + $dbr = wfGetDB( DB_SLAVE ); + $templatelinks = $dbr->tableName( 'templatelinks' ); + $name = $dbr->addQuotes( $this->getName() ); + return "SELECT {$name} AS type, + " . NS_TEMPLATE . " AS namespace, + tl_title AS title, + COUNT(*) AS value + FROM {$templatelinks} + WHERE tl_namespace = " . NS_TEMPLATE . " + GROUP BY 1, 2, 3"; + } + + /** + * Pre-cache page existence to speed up link generation + * + * @param Database $dbr Database connection + * @param int $res Result pointer + */ + public function preprocessResults( $dbr, $res ) { + $batch = new LinkBatch(); + while( $row = $dbr->fetchObject( $res ) ) { + $title = Title::makeTitleSafe( $row->namespace, $row->title ); + $batch->addObj( $title ); + } + $batch->execute(); + if( $dbr->numRows( $res ) > 0 ) + $dbr->dataSeek( $res, 0 ); + } + + /** + * Format a result row + * + * @param Skin $skin Skin to use for UI elements + * @param object $result Result row + * @return string + */ + public function formatResult( $skin, $result ) { + $title = Title::makeTitleSafe( $result->namespace, $result->title ); + if( $title instanceof Title ) { + return wfSpecialList( + $skin->makeLinkObj( $title ), + $this->makeWlhLink( $title, $skin, $result ) + ); + } else { + $tsafe = htmlspecialchars( $result->title ); + return "Invalid title in result set; {$tsafe}"; + } + } + + /** + * Make a "what links here" link for a given title + * + * @param Title $title Title to make the link for + * @param Skin $skin Skin to use + * @param object $result Result row + * @return string + */ + private function makeWlhLink( $title, $skin, $result ) { + global $wgLang; + $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); + $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $result->value ) ); + return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() ); + } + +} + +/** + * Execution function + * + * @param mixed $par Parameters passed to the page + */ +function wfSpecialMostlinkedtemplates( $par = false ) { + list( $limit, $offset ) = wfCheckLimits(); + $mlt = new SpecialMostlinkedtemplates(); + $mlt->doQuery( $offset, $limit ); +} + diff --git a/includes/SpecialMostrevisions.php b/includes/SpecialMostrevisions.php index 59157056..9479a583 100644 --- a/includes/SpecialMostrevisions.php +++ b/includes/SpecialMostrevisions.php @@ -63,4 +63,4 @@ function wfSpecialMostrevisions() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialMovepage.php b/includes/SpecialMovepage.php index d8f01874..cfc434ae 100644 --- a/includes/SpecialMovepage.php +++ b/includes/SpecialMovepage.php @@ -12,7 +12,7 @@ function wfSpecialMovepage( $par = null ) { # Check rights if ( !$wgUser->isAllowed( 'move' ) ) { - $wgOut->showErrorPage( 'movenologin', 'movenologintext' ); + $wgOut->showPermissionsErrorPage( array( $wgUser->isAnon() ? 'movenologintext' : 'movenotallowed' ) ); return; } @@ -105,14 +105,10 @@ class MovePageForm { if ( $err == 'articleexists' && $wgUser->isAllowed( 'delete' ) ) { $wgOut->addWikiText( wfMsg( 'delete_and_move_text', $encNewTitle ) ); $movepagebtn = wfMsgHtml( 'delete_and_move' ); - $confirmText = wfMsgHtml( 'delete_and_move_confirm' ); $submitVar = 'wpDeleteAndMove'; $confirm = " - - + "; $err = ''; } else { @@ -131,7 +127,6 @@ class MovePageForm { $movearticle = wfMsgHtml( 'movearticle' ); $newtitle = wfMsgHtml( 'newtitle' ); - $movetalk = wfMsgHtml( 'movetalk' ); $movereason = wfMsgHtml( 'movereason' ); $titleObj = SpecialPage::getTitleFor( 'Movepage' ); @@ -149,18 +144,18 @@ class MovePageForm {
    ' . $nsForm; - $out2 .= '' . + $out2 .= '
    ' . $nsForm; + $out2 .= '' . $sk->makeKnownLink( $wgContLang->specialPage( "Allpages" ), wfMsgHtml ( 'allpages' ) ); @@ -324,11 +339,16 @@ function showChunk( $namespace = NS_MAIN, $from, $including = false ) { $wgOut->addHtml( $out2 . $out ); if( isset($prevLink) or isset($nextLink) ) { - $wgOut->addHtml( '

    ' ); - if( isset( $prevLink ) ) - $wgOut->addHTML( $prevLink . ' | '); - if( isset( $nextLink ) ) + $wgOut->addHtml( '


    ' ); + if( isset( $prevLink ) ) { + $wgOut->addHTML( $prevLink ); + } + if( isset( $prevLink ) && isset( $nextLink ) ) { + $wgOut->addHTML( ' | ' ); + } + if( isset( $nextLink ) ) { $wgOut->addHTML( $nextLink ); + } $wgOut->addHTML( '

    ' ); } diff --git a/includes/SpecialAncientpages.php b/includes/SpecialAncientpages.php index c0bbb7ba..dee8fbde 100644 --- a/includes/SpecialAncientpages.php +++ b/includes/SpecialAncientpages.php @@ -60,4 +60,4 @@ function wfSpecialAncientpages() { $app->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php index 5f47fa13..942ebe8b 100644 --- a/includes/SpecialBlockip.php +++ b/includes/SpecialBlockip.php @@ -43,6 +43,7 @@ function wfSpecialBlockip( $par ) { */ class IPBlockForm { var $BlockAddress, $BlockExpiry, $BlockReason; +# var $BlockEmail; function IPBlockForm( $par ) { global $wgRequest, $wgUser; @@ -60,12 +61,13 @@ class IPBlockForm { $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault ); $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault ); $this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault ); + $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false ); # Re-check user's rights to hide names, very serious, defaults to 0 - $this->BlockHideName = $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ); + $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0; } function showForm( $err ) { - global $wgOut, $wgUser, $wgSysopUserBans; + global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang; $wgOut->setPagetitle( wfMsg( 'blockip' ) ); $wgOut->addWikiText( wfMsg( 'blockiptext' ) ); @@ -84,6 +86,7 @@ class IPBlockForm { $titleObj = SpecialPage::getTitleFor( 'Blockip' ); $action = $titleObj->escapeLocalURL( "action=submit" ); + $alignRight = $wgContLang->isRtl() ? 'left' : 'right'; if ( "" != $err ) { $wgOut->setSubtitle( wfMsgHtml( 'formerror' ) ); @@ -141,7 +144,7 @@ class IPBlockForm { $blockReasonList .= $optgroup; } - $token = htmlspecialchars( $wgUser->editToken() ); + $token = $wgUser->editToken(); global $wgStylePath, $wgStyleVersion; $wgOut->addHTML( " @@ -150,8 +153,8 @@ class IPBlockForm {
    - - + "); if ($showblockoptions) { $wgOut->addHTML(" - - + - - + @@ -182,8 +185,8 @@ class IPBlockForm { if ( $blockReasonList != '' ) { $wgOut->addHTML(" - - + - - + - - - - "); } + + global $wgSysopEmailBans; + + if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { + $wgOut->addHTML(" + + + + + "); + } $wgOut->addHTML(" -
    {$mIpaddress}: + {$mIpaddress} " . Xml::input( 'wpBlockAddress', 45, $this->BlockAddress, array( 'tabindex' => '1', @@ -162,8 +165,8 @@ class IPBlockForm {
    {$mIpbexpiry}: + {$mIpbexpiry} @@ -173,8 +176,8 @@ class IPBlockForm { $wgOut->addHTML("
    {$mIpbother}: + {$mIpbother} " . Xml::input( 'wpBlockOther', 45, $this->BlockOther, array( 'tabindex' => '3', 'id' => 'mw-bi-other' ) ) . "
    {$mIpbreasonother}: + {$mIpbreasonother} @@ -192,15 +195,16 @@ class IPBlockForm { } $wgOut->addHTML("
    {$mIpbreason}: + {$mIpbreason} " . Xml::input( 'wpBlockReason', 45, $this->BlockReason, - array( 'tabindex' => '5', 'id' => 'mw-bi-reason' ) ) . " + array( 'tabindex' => '5', 'id' => 'mw-bi-reason', + 'maxlength'=> '200' ) ) . "
      + " . wfCheckLabel( wfMsgHtml( 'ipbanononly' ), 'wpAnonOnly', 'wpAnonOnly', $this->BlockAnonOnly, array( 'tabindex' => '6' ) ) . " @@ -208,7 +212,7 @@ class IPBlockForm {
      + " . wfCheckLabel( wfMsgHtml( 'ipbcreateaccount' ), 'wpCreateAccount', 'wpCreateAccount', $this->BlockCreateAccount, array( 'tabindex' => '7' ) ) . " @@ -216,7 +220,7 @@ class IPBlockForm {
      + " . wfCheckLabel( wfMsgHtml( 'ipbenableautoblock' ), 'wpEnableAutoblock', 'wpEnableAutoblock', $this->BlockEnableAutoblock, array( 'tabindex' => '8' ) ) . " @@ -228,7 +232,7 @@ class IPBlockForm { $wgOut->addHTML("
      + " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ), 'wpHideName', 'wpHideName', $this->BlockHideName, array( 'tabindex' => '9' ) ) . " @@ -236,12 +240,27 @@ class IPBlockForm {
      + " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ), + 'wpEmailBan', 'wpEmailBan', $this->BlockEmail, + array( 'tabindex' => '10' )) . " +
      - " . Xml::submitButton( wfMsgHtml( 'ipbsubmit' ), - array( 'name' => 'wpBlock', 'tabindex' => '10' ) ) . " + + " . Xml::submitButton( wfMsg( 'ipbsubmit' ), + array( 'name' => 'wpBlock', 'tabindex' => '11' ) ) . "
    " . @@ -354,10 +373,10 @@ class IPBlockForm { # Create block # Note: for a user block, ipb_address is only for display purposes - $block = new Block( $this->BlockAddress, $userId, $wgUser->getID(), $reasonstr, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, - $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName); + $this->BlockCreateAccount, $this->BlockEnableAutoblock, $this->BlockHideName, + $this->BlockEmail); if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) { @@ -418,6 +437,8 @@ class IPBlockForm { $flags[] = 'nocreate'; if( !$this->BlockEnableAutoblock ) $flags[] = 'noautoblock'; + if ( $this->BlockEmail ) + $flags[] = 'noemail'; return implode( ',', $flags ); } @@ -471,4 +492,4 @@ class IPBlockForm { } } } -?> + diff --git a/includes/SpecialBlockme.php b/includes/SpecialBlockme.php index c2cb1a58..da2757ac 100644 --- a/includes/SpecialBlockme.php +++ b/includes/SpecialBlockme.php @@ -36,4 +36,4 @@ function wfSpecialBlockme() { $wgOut->addWikiText( $success ); } -?> + diff --git a/includes/SpecialBooksources.php b/includes/SpecialBooksources.php index d3136ea4..5f103495 100644 --- a/includes/SpecialBooksources.php +++ b/includes/SpecialBooksources.php @@ -14,20 +14,20 @@ class SpecialBookSources extends SpecialPage { * ISBN passed to the page, if any */ private $isbn = ''; - + /** * Constructor */ public function __construct() { parent::__construct( 'Booksources' ); } - + /** * Show the special page * * @param $isbn ISBN passed as a subpage parameter */ - public function execute( $isbn = false ) { + public function execute( $isbn ) { global $wgOut, $wgRequest; $this->setHeaders(); $this->isbn = $this->cleanIsbn( $isbn ? $isbn : $wgRequest->getText( 'isbn' ) ); @@ -36,7 +36,7 @@ class SpecialBookSources extends SpecialPage { if( strlen( $this->isbn ) > 0 ) $this->showList(); } - + /** * Trim ISBN and remove characters which aren't required * @@ -46,7 +46,7 @@ class SpecialBookSources extends SpecialPage { private function cleanIsbn( $isbn ) { return trim( preg_replace( '![^0-9X]!', '', $isbn ) ); } - + /** * Generate a form to allow users to enter an ISBN * @@ -64,7 +64,7 @@ class SpecialBookSources extends SpecialPage { $form .= ''; return $form; } - + /** * Determine where to get the list of book sources from, * format and output them @@ -73,19 +73,19 @@ class SpecialBookSources extends SpecialPage { */ private function showList() { global $wgOut, $wgContLang; - + # Hook to allow extensions to insert additional HTML, # e.g. for API-interacting plugins and so on wfRunHooks( 'BookInformation', array( $this->isbn, &$wgOut ) ); - + # Check for a local page such as Project:Book_sources and use that if available - $title = Title::makeTitleSafe( NS_PROJECT, wfMsg( 'booksources' ) ); # Should this be wfMsgForContent()? -- RC + $title = Title::makeTitleSafe( NS_PROJECT, wfMsgForContent( 'booksources' ) ); # Show list in content language if( is_object( $title ) && $title->exists() ) { $rev = Revision::newFromTitle( $title ); $wgOut->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); return true; } - + # Fall back to the defaults given in the language file $wgOut->addWikiText( wfMsgNoTrans( 'booksources-text' ) ); $wgOut->addHtml( '
      ' ); @@ -110,4 +110,4 @@ class SpecialBookSources extends SpecialPage { } -?> + diff --git a/includes/SpecialBrokenRedirects.php b/includes/SpecialBrokenRedirects.php index 208a7e1f..1fb48350 100644 --- a/includes/SpecialBrokenRedirects.php +++ b/includes/SpecialBrokenRedirects.php @@ -20,8 +20,7 @@ class BrokenRedirectsPage extends PageQueryPage { function isSyndicated() { return false; } function getPageHeader( ) { - global $wgOut; - return $wgOut->parse( wfMsg( 'brokenredirectstext' ) ); + return wfMsgExt( 'brokenredirectstext', array( 'parse' ) ); } function getSQL() { @@ -36,7 +35,8 @@ class BrokenRedirectsPage extends PageQueryPage { FROM $redirect AS rd JOIN $page p1 ON (rd.rd_from=p1.page_id) LEFT JOIN $page AS p2 ON (rd_namespace=p2.page_namespace AND rd_title=p2.page_title ) - WHERE p2.page_namespace IS NULL"; + WHERE rd_namespace >= 0 + AND p2.page_namespace IS NULL"; return $sql; } @@ -92,4 +92,4 @@ function wfSpecialBrokenRedirects() { return $sbr->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialCategories.php b/includes/SpecialCategories.php index 45e1ae6c..596569ed 100644 --- a/includes/SpecialCategories.php +++ b/includes/SpecialCategories.php @@ -53,14 +53,11 @@ class CategoryPager extends AlphabeticPager { function formatRow($result) { global $wgLang; $title = Title::makeTitle( NS_CATEGORY, $result->cl_to ); - return ( - '
    • ' . - $this->getSkin()->makeLinkObj( $title, $title->getText() ) - . ' ' . - wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), - $wgLang->formatNum( $result->count ) ) - . "
    • \n" ); + $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) ); + $count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'), + $wgLang->formatNum( $result->count ) ); + return Xml::tags('li', null, "$titleText ($count)" ) . "\n"; } } -?> + diff --git a/includes/SpecialConfirmemail.php b/includes/SpecialConfirmemail.php index 58e55899..ba419f25 100644 --- a/includes/SpecialConfirmemail.php +++ b/includes/SpecialConfirmemail.php @@ -1,23 +1,21 @@ execute( $par ); -} - /** * Special page allows users to request email confirmation message, and handles * processing of the confirmation code when the link in the email is followed * * @addtogroup SpecialPage + * @author Brion Vibber * @author Rob Church */ -class EmailConfirmation extends SpecialPage { +class EmailConfirmation extends UnlistedSpecialPage { + + /** + * Constructor + */ + public function __construct() { + parent::__construct( 'Confirmemail' ); + } /** * Main execution point @@ -26,6 +24,7 @@ class EmailConfirmation extends SpecialPage { */ function execute( $code ) { global $wgUser, $wgOut; + $this->setHeaders(); if( empty( $code ) ) { if( $wgUser->isLoggedIn() ) { if( User::isValidEmailAddr( $wgUser->getEmail() ) ) { @@ -102,4 +101,4 @@ class EmailConfirmation extends SpecialPage { } -?> + diff --git a/includes/SpecialContributions.php b/includes/SpecialContributions.php index 82c8d608..cc1b2e6f 100644 --- a/includes/SpecialContributions.php +++ b/includes/SpecialContributions.php @@ -9,15 +9,21 @@ class ContribsPager extends IndexPager { var $messages, $target; var $namespace = '', $mDb; - function __construct( $target, $namespace = false ) { - global $wgUser; - + function __construct( $target, $namespace = false, $year = false, $month = false ) { parent::__construct(); foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) { $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') ); } $this->target = $target; $this->namespace = $namespace; + + $year = intval($year); + $month = intval($month); + + $this->year = ($year > 0 && $year < 10000) ? $year : false; + $this->month = ($month > 0 && $month < 13) ? $month : false; + $this->getDateCond(); + $this->mDb = wfGetDB( DB_SLAVE, 'contributions' ); } @@ -29,7 +35,7 @@ class ContribsPager extends IndexPager { function getQueryInfo() { list( $index, $userCond ) = $this->getUserCond(); - $conds = array_merge( array( 'page_id=rev_page' ), $userCond, $this->getNamespaceCond() ); + $conds = array_merge( array('page_id=rev_page'), $userCond, $this->getNamespaceCond() ); return array( 'tables' => array( 'page', 'revision' ), @@ -39,7 +45,7 @@ class ContribsPager extends IndexPager { 'rev_user_text', 'rev_deleted' ), 'conds' => $conds, - 'options' => array( 'FORCE INDEX' => $index ) + 'options' => array( 'USE INDEX' => $index ) ); } @@ -64,6 +70,33 @@ class ContribsPager extends IndexPager { return array(); } } + + function getDateCond() { + if ( $this->year || $this->month ) { + // Assume this year if only a month is given + if ( $this->year ) { + $year_start = $this->year; + } else { + $year_start = substr( wfTimestampNow(), 0, 4 ); + $thisMonth = gmdate( 'n' ); + if( $this->month > $thisMonth ) { + // Future contributions aren't supposed to happen. :) + $year_start--; + } + } + + if ( $this->month ) { + $month_end = str_pad($this->month + 1, 2, '0', STR_PAD_LEFT); + $year_end = $year_start; + } else { + $month_end = 0; + $year_end = $year_start + 1; + } + $ts_end = str_pad($year_end . $month_end, 14, '0' ); + + $this->mOffset = $ts_end; + } + } function getIndexField() { return 'rev_timestamp'; @@ -110,7 +143,7 @@ class ContribsPager extends IndexPager { function formatRow( $row ) { wfProfileIn( __METHOD__ ); - global $wgLang, $wgUser; + global $wgLang, $wgUser, $wgContLang; $sk = $this->getSkin(); $rev = new Revision( $row ); @@ -138,8 +171,15 @@ class ContribsPager extends IndexPager { } $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')'; - $comment = $sk->revComment( $rev ); + $comment = $wgContLang->getDirMark() . $sk->revComment( $rev ); $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true ); + + if( $this->target == 'newbies' ) { + $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text ); + $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') '; + } else { + $userlink = ''; + } if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $d = '' . $d . ''; @@ -151,7 +191,7 @@ class ContribsPager extends IndexPager { $mflag = ''; } - $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}"; + $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link}{$userlink}{$comment} {$topmarktext}"; if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { $ret .= ' ' . wfMsgHtml( 'deletedrev' ); } @@ -159,6 +199,16 @@ class ContribsPager extends IndexPager { wfProfileOut( __METHOD__ ); return $ret; } + + /** + * Get the Database object in use + * + * @return Database + */ + public function getDatabase() { + return $this->mDb; + } + } /** @@ -218,16 +268,48 @@ function wfSpecialContributions( $par = null ) { if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) { $options['bot'] = '1'; } + + $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev'; + # Offset overrides year/month selection + if ( ( $month = $wgRequest->getIntOrNull( 'month' ) ) !== null && $month !== -1 ) { + $options['month'] = intval( $month ); + } else { + $options['month'] = ''; + } + if ( ( $year = $wgRequest->getIntOrNull( 'year' ) ) !== null ) { + $options['year'] = intval( $year ); + } else if( $options['month'] ) { + $thisMonth = intval( gmdate( 'n' ) ); + $thisYear = intval( gmdate( 'Y' ) ); + if( intval( $options['month'] ) > $thisMonth ) { + $thisYear--; + } + $options['year'] = $thisYear; + } else { + $options['year'] = ''; + } wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id ); $wgOut->addHTML( contributionsForm( $options ) ); + # Show original selected options, don't apply them so as to allow paging + $_GET['year'] = ''; // hack for Pager + $_GET['month'] = ''; // hack for Pager + if( $skip ) { + $options['year'] = ''; + $options['month'] = ''; + } - $pager = new ContribsPager( $target, $options['namespace'] ); + $pager = new ContribsPager( $target, $options['namespace'], $options['year'], $options['month'] ); if ( !$pager->getNumRows() ) { $wgOut->addWikiText( wfMsg( 'nocontribs' ) ); return; } + + # Show a message about slave lag, if applicable + if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) + $wgOut->showLagWarning( $lag ); + $wgOut->addHTML( '

      ' . $pager->getNavigationBar() . '

      ' . $pager->getBody() . @@ -279,6 +361,9 @@ function contributionsSub( $nt, $id ) { } # Other logs link $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() ); + + wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) ); + $links = implode( ' | ', $tools ); } @@ -314,6 +399,14 @@ function contributionsForm( $options ) { if ( !isset( $options['contribs'] ) ) { $options['contribs'] = 'user'; } + + if ( !isset( $options['year'] ) ) { + $options['year'] = ''; + } + + if ( !isset( $options['month'] ) ) { + $options['month'] = ''; + } if ( $options['contribs'] == 'newbie' ) { $options['target'] = ''; @@ -322,7 +415,7 @@ function contributionsForm( $options ) { $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); foreach ( $options as $name => $value ) { - if ( in_array( $name, array( 'namespace', 'target', 'contribs' ) ) ) { + if ( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) { continue; } $f .= "\t" . Xml::hidden( $name, $value ) . "\n"; @@ -335,11 +428,19 @@ function contributionsForm( $options ) { Xml::input( 'target', 20, $options['target']) . ' '. Xml::label( wfMsg( 'namespace' ), 'namespace' ) . Xml::namespaceSelector( $options['namespace'], '' ) . + Xml::openElement( 'p' ) . + Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + Xml::input( 'year', 4, $options['year'], array('id' => 'year', 'maxlength' => 4) ) . ' '. + Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::monthSelector( $options['month'], -1 ) . ' '. Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) . - '' . + Xml::closeElement( 'p' ); + + $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' ); + if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) ) + $f .= "

      {$explain}

      "; + + $f .= '' . Xml::closeElement( 'form' ); return $f; } - - -?> diff --git a/includes/SpecialDeadendpages.php b/includes/SpecialDeadendpages.php index 48d27add..0d94161b 100644 --- a/includes/SpecialDeadendpages.php +++ b/includes/SpecialDeadendpages.php @@ -15,7 +15,7 @@ class DeadendPagesPage extends PageQueryPage { } function getPageHeader() { - return '

      ' . wfMsg('deadendpagestext') . '

      '; + return wfMsgExt( 'deadendpagestext', array( 'parse' ) ); } /** @@ -62,4 +62,4 @@ function wfSpecialDeadendpages() { return $depp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialDisambiguations.php b/includes/SpecialDisambiguations.php index da0562ab..fb1d75e9 100644 --- a/includes/SpecialDisambiguations.php +++ b/includes/SpecialDisambiguations.php @@ -15,8 +15,7 @@ class DisambiguationsPage extends PageQueryPage { function getPageHeader( ) { - global $wgOut; - return $wgOut->parse( wfMsg( 'disambiguations-text' ) ); + return wfMsgExt( 'disambiguations-text', array( 'parse' ) ); } function getSQL() { @@ -105,4 +104,3 @@ function wfSpecialDisambiguations() { return $sd->doQuery( $offset, $limit ); } -?> \ No newline at end of file diff --git a/includes/SpecialDoubleRedirects.php b/includes/SpecialDoubleRedirects.php index e7b355c5..6d46fc50 100644 --- a/includes/SpecialDoubleRedirects.php +++ b/includes/SpecialDoubleRedirects.php @@ -19,8 +19,7 @@ class DoubleRedirectsPage extends PageQueryPage { function isSyndicated() { return false; } function getPageHeader( ) { - #FIXME : probably need to add a backlink to the maintenance page. - return '

      '.wfMsg("doubleredirectstext")."


      \n"; + return wfMsgExt( 'doubleredirectstext', array( 'parse' ) ); } function getSQLText( &$dbr, $namespace = null, $title = null ) { @@ -64,6 +63,7 @@ class DoubleRedirectsPage extends PageQueryPage { $fname = 'DoubleRedirectsPage::formatResult'; $titleA = Title::makeTitle( $result->namespace, $result->title ); + $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' ); if ( $result && !isset( $result->nsb ) ) { $dbr = wfGetDB( DB_SLAVE ); @@ -75,13 +75,12 @@ class DoubleRedirectsPage extends PageQueryPage { } } if ( !$result ) { - return ''; + return "{$linkA}\n"; } $titleB = Title::makeTitle( $result->nsb, $result->tb ); $titleC = Title::makeTitle( $result->nsc, $result->tc ); - $linkA = $skin->makeKnownLinkObj( $titleA,'', 'redirect=no' ); $edit = $skin->makeBrokenLinkObj( $titleA, "(".wfMsg("qbedit").")" , 'redirect=no'); $linkB = $skin->makeKnownLinkObj( $titleB, '', 'redirect=no' ); $linkC = $skin->makeKnownLinkObj( $titleC ); @@ -102,4 +101,4 @@ function wfSpecialDoubleRedirects() { return $sdr->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialEmailuser.php b/includes/SpecialEmailuser.php index 900a2c32..1d5a9647 100644 --- a/includes/SpecialEmailuser.php +++ b/includes/SpecialEmailuser.php @@ -45,6 +45,13 @@ function wfSpecialEmailuser( $par ) { return; } + if ( $wgUser->isBlockedFromEmailUser() ) { + // User has been blocked from sending e-mail. Show the std blocked form. + wfDebug( "User is blocked from sending e-mail.\n" ); + $wgOut->blockedPage(); + return; + } + $f = new EmailUserForm( $nu ); if ( "success" == $action ) { @@ -108,7 +115,7 @@ class EmailUserForm { $titleObj = SpecialPage::getTitleFor( "Emailuser" ); $action = $titleObj->escapeLocalURL( "target=" . urlencode( $this->target->getName() ) . "&action=submit" ); - $token = $wgUser->editToken(); + $token = htmlspecialchars( $wgUser->editToken() ); $wgOut->addHTML( " @@ -185,4 +192,4 @@ class EmailUserForm { $wgOut->returnToMain( false, $user->getUserPage() ); } } -?> + diff --git a/includes/SpecialExport.php b/includes/SpecialExport.php index a597fdd0..12bd4d5c 100644 --- a/includes/SpecialExport.php +++ b/includes/SpecialExport.php @@ -53,7 +53,7 @@ function wfExportGetPagesFromCategory( $title ) { * */ function wfSpecialExport( $page = '' ) { - global $wgOut, $wgRequest, $wgExportAllowListContributors; + global $wgOut, $wgRequest, $wgSitename, $wgExportAllowListContributors; global $wgExportAllowHistory, $wgExportMaxHistory; $curonly = true; @@ -71,7 +71,7 @@ function wfSpecialExport( $page = '' ) { } } } - else if( $wgRequest->wasPosted() ) { + else if( $wgRequest->wasPosted() && $page == '' ) { $page = $wgRequest->getText( 'pages' ); $curonly = $wgRequest->getCheck( 'curonly' ); $rawOffset = $wgRequest->getVal( 'offset' ); @@ -131,6 +131,11 @@ function wfSpecialExport( $page = '' ) { // This should provide safer streaming for pages with history wfResetOutputBuffers(); header( "Content-type: application/xml; charset=utf-8" ); + if( $wgRequest->getCheck( 'wpDownload' ) ) { + // Provide a sane filename suggestion + $filename = urlencode( $wgSitename . '-' . wfTimestampNow() . '.xml' ); + $wgRequest->response()->header( "Content-disposition: attachment;filename={$filename}" ); + } $pages = explode( "\n", $page ); $db = wfGetDB( DB_SLAVE ); @@ -164,25 +169,28 @@ function wfSpecialExport( $page = '' ) { return; } - $wgOut->addWikiText( wfMsg( "exporttext" ) ); - $titleObj = SpecialPage::getTitleFor( "Export" ); + $self = SpecialPage::getTitleFor( 'Export' ); + $wgOut->addHtml( wfMsgExt( 'exporttext', 'parse' ) ); + + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $self->getLocalUrl( 'action=submit' ) ) ); + + $form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . ' '; + $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '
      '; + + $form .= Xml::openElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) ); + $form .= htmlspecialchars( $page ); + $form .= Xml::closeElement( 'textarea' ); + $form .= '
      '; - $form = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalUrl() ) ); - - $form .= wfInputLabel( wfMsg( 'export-addcattext' ), 'catname', 'catname', 40 ) . ' '; - $form .= wfSubmitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '
      '; - - $form .= wfOpenElement( 'textarea', array( 'name' => 'pages', 'cols' => 40, 'rows' => 10 ) ) . htmlspecialchars($page). '
      '; - if( $wgExportAllowHistory ) { - $form .= wfCheck( 'curonly', true, array( 'value' => 'true', 'id' => 'curonly' ) ); - $form .= wfLabel( wfMsg( 'exportcuronly' ), 'curonly' ) . '
      '; + $form .= Xml::checkLabel( wfMsg( 'exportcuronly' ), 'curonly', 'curonly', true ) . '
      '; } else { - $wgOut->addWikiText( wfMsg( 'exportnohistory' ) ); + $wgOut->addHtml( wfMsgExt( 'exportnohistory', 'parse' ) ); } - $form .= wfHidden( 'action', 'submit' ); - $form .= wfSubmitButton( wfMsg( 'export-submit' ) ) . ''; + $form .= Xml::checkLabel( wfMsg( 'export-download' ), 'wpDownload', 'wpDownload', true ) . '
      '; + + $form .= Xml::submitButton( wfMsg( 'export-submit' ) ); + $form .= Xml::closeElement( 'form' ); $wgOut->addHtml( $form ); -} - -?> +} \ No newline at end of file diff --git a/includes/SpecialFewestrevisions.php b/includes/SpecialFewestrevisions.php index 4c0cd686..ba6db8b6 100644 --- a/includes/SpecialFewestrevisions.php +++ b/includes/SpecialFewestrevisions.php @@ -62,4 +62,4 @@ function wfSpecialFewestrevisions() { $frp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialImagelist.php b/includes/SpecialImagelist.php index 92b9ae11..1688fe7c 100644 --- a/includes/SpecialImagelist.php +++ b/includes/SpecialImagelist.php @@ -57,7 +57,6 @@ class ImageListPager extends TablePager { function getFieldNames() { if ( !$this->mFieldNames ) { $this->mFieldNames = array( - 'links' => '', 'img_timestamp' => wfMsg( 'imagelist_date' ), 'img_name' => wfMsg( 'imagelist_name' ), 'img_user_text' => wfMsg( 'imagelist_user' ), @@ -75,7 +74,6 @@ class ImageListPager extends TablePager { function getQueryInfo() { $fields = $this->getFieldNames(); - unset( $fields['links'] ); $fields = array_keys( $fields ); $fields[] = 'img_user'; return array( @@ -112,17 +110,15 @@ class ImageListPager extends TablePager { function formatValue( $field, $value ) { global $wgLang; switch ( $field ) { - case 'links': - $name = $this->mCurrentRow->img_name; - $ilink = "" . $this->mMessages['imgfile'] . ""; - $desc = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), - $this->mMessages['imgdesc'] ); - return "$desc | $ilink"; case 'img_timestamp': return $wgLang->timeanddate( $value, true ); case 'img_name': - return htmlspecialchars( $value ); + $name = $this->mCurrentRow->img_name; + $link = $this->getSkin()->makeKnownLinkObj( Title::makeTitle( NS_IMAGE, $name ), $value ); + $image = wfLocalFile( $value ); + $url = $image->getURL(); + $download = Xml::element('a', array( "href" => $url ), $this->mMessages['imgfile'] ); + return "$link ($download)"; case 'img_user_text': if ( $this->mCurrentRow->img_user ) { $link = $this->getSkin()->makeLinkObj( Title::makeTitle( NS_USER, $value ), @@ -132,7 +128,7 @@ class ImageListPager extends TablePager { } return $link; case 'img_size': - return $wgLang->formatNum( $value ); + return $this->getSkin()->formatSize( $value ); case 'img_description': return $this->getSkin()->commentBlock( $value ); } @@ -167,4 +163,4 @@ class ImageListPager extends TablePager { } } -?> + diff --git a/includes/SpecialImport.php b/includes/SpecialImport.php index c7b861d0..ad5d8e64 100644 --- a/includes/SpecialImport.php +++ b/includes/SpecialImport.php @@ -85,7 +85,7 @@ function wfSpecialImport( $page = '' ) { } } - $action = $wgTitle->escapeLocalUrl( 'action=submit' ); + $action = $wgTitle->getLocalUrl( 'action=submit' ); if( $wgUser->isAllowed( 'importupload' ) ) { $wgOut->addWikiText( wfMsg( "importtext" ) ); @@ -209,6 +209,9 @@ class ImportReporter { $nullRevision = Revision::newNullRevision( $dbw, $title->getArticleId(), $comment, true ); $nullRevision->insertOn( $dbw ); + # Update page record + $article = new Article( $title ); + $article->updateRevisionOn( $dbw, $nullRevision ); } } @@ -857,13 +860,22 @@ class ImportStreamSource { } } - function newFromURL( $url ) { + function newFromURL( $url, $method = 'GET' ) { wfDebug( __METHOD__ . ": opening $url\n" ); - # fopen-wrappers are normally turned off for security. - ini_set( "allow_url_fopen", true ); - $ret = ImportStreamSource::newFromFile( $url ); - ini_set( "allow_url_fopen", false ); - return $ret; + # Use the standard HTTP fetch function; it times out + # quicker and sorts out user-agent problems which might + # otherwise prevent importing from large sites, such + # as the Wikimedia cluster, etc. + $data = Http::request( $method, $url ); + if( $data !== false ) { + $file = tmpfile(); + fwrite( $file, $data ); + fflush( $file ); + fseek( $file, 0 ); + return new ImportStreamSource( $file ); + } else { + return new WikiErrorMsg( 'importcantopen' ); + } } public static function newFromInterwiki( $interwiki, $page, $history=false ) { @@ -873,10 +885,11 @@ class ImportStreamSource { } else { $params = $history ? 'history=1' : ''; $url = $link->getFullUrl( $params ); - return ImportStreamSource::newFromURL( $url ); + # For interwikis, use POST to avoid redirects. + return ImportStreamSource::newFromURL( $url, "POST" ); } } } -?> + diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php index 8cb5729e..4f093dcb 100644 --- a/includes/SpecialIpblocklist.php +++ b/includes/SpecialIpblocklist.php @@ -18,30 +18,49 @@ function wfSpecialIpblocklist() { $ipu = new IPUnblockForm( $ip, $id, $reason ); - if ( "success" == $action ) { - $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) ); - } else if ( "submit" == $action && $wgRequest->wasPosted() && - $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { - if ( ! $wgUser->isAllowed('block') ) { + if( $action == 'unblock' ) { + # Check permissions + if( !$wgUser->isAllowed( 'block' ) ) { $wgOut->permissionRequired( 'block' ); return; } - # Can't unblock when the database is locked + # Check for database lock if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - $ipu->doSubmit(); - } else if ( "unblock" == $action ) { - # Can't unblock when the database is locked + # Show unblock form + $ipu->showForm( '' ); + } elseif( $action == 'submit' && $wgRequest->wasPosted() + && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { + # Check permissions + if( !$wgUser->isAllowed( 'block' ) ) { + $wgOut->permissionRequired( 'block' ); + return; + } + # Check for database lock if( wfReadOnly() ) { $wgOut->readOnlyPage(); return; } - $ipu->showForm( "" ); + # Remove blocks and redirect user to success page + $ipu->doSubmit(); + } elseif( $action == 'success' ) { + # Inform the user of a successful unblock + # (No need to check permissions or locks here, + # if something was done, then it's too late!) + if ( substr( $successip, 0, 1) == '#' ) { + // A block ID was unblocked + $ipu->showList( $wgOut->parse( wfMsg( 'unblocked-id', $successip ) ) ); + } else { + // A username/IP was unblocked + $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) ); + } } else { - $ipu->showList( "" ); + # Just show the block list + $ipu->showList( '' ); } + } /** @@ -58,7 +77,7 @@ class IPUnblockForm { } function showForm( $err ) { - global $wgOut, $wgUser, $wgSysopUserBans; + global $wgOut, $wgUser, $wgSysopUserBans, $wgContLang; $wgOut->setPagetitle( wfMsg( 'unblockip' ) ); $wgOut->addWikiText( wfMsg( 'unblockiptext' ) ); @@ -67,7 +86,8 @@ class IPUnblockForm { $ipr = wfMsgHtml( 'ipbreason' ); $ipus = wfMsgHtml( 'ipusubmit' ); $titleObj = SpecialPage::getTitleFor( "Ipblocklist" ); - $action = $titleObj->escapeLocalURL( "action=submit" ); + $action = $titleObj->getLocalURL( "action=submit" ); + $alignRight = $wgContLang->isRtl() ? 'left' : 'right'; if ( "" != $err ) { $wgOut->setSubtitle( wfMsg( "formerror" ) ); @@ -80,39 +100,43 @@ class IPUnblockForm { $block = Block::newFromID( $this->id ); if ( $block ) { $encName = htmlspecialchars( $block->getRedactedName() ); - $encId = htmlspecialchars( $this->id ); - $addressPart = $encName . ""; + $encId = $this->id; + $addressPart = $encName . Xml::hidden( 'id', $encId ); } } if ( !$addressPart ) { - $addressPart = "ip ) . "\" />"; + $addressPart = Xml::input( 'wpUnblockAddress', 20, $this->ip, array( 'type' => 'text', 'tabindex' => '1' ) ); } - $wgOut->addHTML( " -
      - - - - - - - - - - - - - -
      {$ipa}: - {$addressPart} -
      {$ipr}: - reason ) . "\" /> -
        - -
      - -
      \n" ); + $wgOut->addHTML( + Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'unblockip' ) ) . + Xml::openElement( 'table', array( 'border' => '0' ) ). + "
    + {$ipa} + + {$addressPart} +
    + {$ipr} + " . + Xml::input( 'wpUnblockReason', 40, $this->reason, array( 'type' => 'text', 'tabindex' => '2' ) ) . + "
     " . + Xml::submitButton( $ipus, array( 'name' => 'wpBlock', 'tabindex' => '3' ) ) . + "
    - - " . Xml::checkLabel( wfMsg( 'delete_and_move_confirm' ), 'wpConfirm', 'wpConfirm' ) . "
    - + - + - + @@ -169,20 +164,16 @@ class MovePageForm { if ( $considerTalk ) { $wgOut->addHTML( " - - + " ); } - + $watchChecked = $this->watch || $wgUser->getBoolOption( 'watchmoves' ) || $ot->userIsWatching(); $watch = ''; - $watch .= "'; - $watch .= ''; + $watch .= ''; $watch .= ''; $wgOut->addHtml( $watch ); - + $wgOut->addHTML( " {$confirm} @@ -275,32 +266,38 @@ class MovePageForm { } function showSuccess() { - global $wgOut, $wgRequest, $wgRawHtml; + global $wgOut, $wgRequest, $wgUser; + + $old = Title::newFromText( $wgRequest->getVal( 'oldtitle' ) ); + $new = Title::newFromText( $wgRequest->getVal( 'newtitle' ) ); + + if( is_null( $old ) || is_null( $new ) ) { + throw new ErrorPageError( 'badtitle', 'badtitletext' ); + } $wgOut->setPagetitle( wfMsg( 'movepage' ) ); $wgOut->setSubtitle( wfMsg( 'pagemovedsub' ) ); - $oldText = wfEscapeWikiText( $wgRequest->getVal('oldtitle') ); - $newText = wfEscapeWikiText( $wgRequest->getVal('newtitle') ); - $talkmoved = $wgRequest->getVal('talkmoved'); + $talkmoved = $wgRequest->getVal( 'talkmoved' ); + $oldUrl = $old->getFullUrl( 'redirect=no' ); + $newUrl = $new->getFullURl(); + $oldText = $old->getPrefixedText(); + $newText = $new->getPrefixedText(); + $oldLink = "[$oldUrl $oldText]"; + $newLink = "[$newUrl $newText]"; - $text = wfMsg( 'pagemovedtext', $oldText, $newText ); - - $allowHTML = $wgRawHtml; - $wgRawHtml = false; - $wgOut->addWikiText( $text ); - $wgRawHtml = $allowHTML; + $s = wfMsg( 'movepage-moved', $oldLink, $newLink, $oldText, $newText ); if ( $talkmoved == 1 ) { - $wgOut->addWikiText( wfMsg( 'talkpagemoved' ) ); + $s .= "\n\n" . wfMsg( 'talkpagemoved' ); } elseif( 'articleexists' == $talkmoved ) { - $wgOut->addWikiText( wfMsg( 'talkexists' ) ); + $s .= "\n\n" . wfMsg( 'talkexists' ); } else { - $oldTitle = Title::newFromText( $oldText ); - if ( isset( $oldTitle ) && !$oldTitle->isTalkPage() && $talkmoved != 'notalkpage' ) { - $wgOut->addWikiText( wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ) ); + if( !$old->isTalkPage() && $talkmoved != 'notalkpage' ) { + $s .= "\n\n" . wfMsg( 'talkpagenotmoved', wfMsg( $talkmoved ) ); } } + $wgOut->addWikiText( $s ); } function showLogFragment( $title, &$out ) { @@ -311,4 +308,4 @@ class MovePageForm { } } -?> + diff --git a/includes/SpecialNewimages.php b/includes/SpecialNewimages.php index 72b169b1..f81a70f4 100644 --- a/includes/SpecialNewimages.php +++ b/includes/SpecialNewimages.php @@ -135,10 +135,9 @@ function wfSpecialNewimages( $par, $specialPage ) { $ut = $s->img_user_text; $nt = Title::newFromText( $name, NS_IMAGE ); - $img = new Image( $nt ); $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut ); - $gallery->add( $img, "$ul
    \n".$wgLang->timeanddate( $s->img_timestamp, true )."
    \n" ); + $gallery->add( $nt, "$ul
    \n".$wgLang->timeanddate( $s->img_timestamp, true )."
    \n" ); $timestamp = wfTimestamp( TS_MW, $s->img_timestamp ); if( empty( $firstTimestamp ) ) { @@ -205,4 +204,4 @@ function wfSpecialNewimages( $par, $specialPage ) { } } -?> + diff --git a/includes/SpecialNewpages.php b/includes/SpecialNewpages.php index 48037a73..abd5e018 100644 --- a/includes/SpecialNewpages.php +++ b/includes/SpecialNewpages.php @@ -36,12 +36,19 @@ class NewPagesPage extends QueryPage { } } + private function makeNamespaceWhere() { + return $this->namespace !== 'all' + ? ' AND rc_namespace = ' . intval( $this->namespace ) + : ''; + } + function getSQL() { global $wgUser, $wgUseRCPatrol; $usepatrol = ( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) ? 1 : 0; $dbr = wfGetDB( DB_SLAVE ); list( $recentchanges, $page ) = $dbr->tableNamesN( 'recentchanges', 'page' ); + $nsfilter = $this->makeNamespaceWhere(); $uwhere = $this->makeUserWhere( $dbr ); # FIXME: text will break with compression @@ -62,7 +69,8 @@ class NewPagesPage extends QueryPage { page_latest as rev_id FROM $recentchanges,$page WHERE rc_cur_id=page_id AND rc_new=1 - AND rc_namespace=" . $this->namespace . " AND page_is_redirect=0 + {$nsfilter} + AND page_is_redirect = 0 {$uwhere}"; } @@ -130,11 +138,13 @@ class NewPagesPage extends QueryPage { * @return string */ function getPageHeader() { + global $wgScript; $self = SpecialPage::getTitleFor( $this->getName() ); - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) ); + $form = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); + $form .= Xml::hidden( 'title', $self->getPrefixedDBkey() ); # Namespace selector $form .= '
    {$movearticle}:{$movearticle} {$oldTitle}



    - - " . Xml::checkLabel( wfMsg( 'movetalk' ), 'wpMovetalk', 'wpMovetalk', $moveTalkChecked ) . "
    " . Xml::check( 'wpWatch', $watchChecked, array( 'id' => 'watch' ) ) . '' . Xml::label( wfMsg( 'move-watch' ), 'watch' ) . '' . Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) . '
    '; - $form .= ''; + $form .= ''; # Username filter $form .= ''; $form .= ''; @@ -186,7 +196,7 @@ function wfSpecialNewpages($par, $specialPage) { } } } else { - if( $ns = $wgRequest->getInt( 'namespace', 0 ) ) + if( $ns = $wgRequest->getText( 'namespace', NS_MAIN ) ) $namespace = $ns; if( $un = $wgRequest->getText( 'username' ) ) $username = $un; @@ -199,6 +209,4 @@ function wfSpecialNewpages($par, $specialPage) { if ( ! $npp->doFeed( $wgRequest->getVal( 'feed' ), $limit ) ) $npp->doQuery( $offset, $limit, $shownavigation ); -} - -?> +} \ No newline at end of file diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index cf882509..89fd15bb 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -94,12 +94,14 @@ class SpecialPage 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ), 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), + 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ), 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), + 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ), 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ), 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ), @@ -177,7 +179,7 @@ class SpecialPage } if( $wgEmailAuthentication ) { - self::$mList['Confirmemail'] = array( 'UnlistedSpecialPage', 'Confirmemail' ); + self::$mList['Confirmemail'] = 'EmailConfirmation'; } # Add extension special pages @@ -274,6 +276,30 @@ class SpecialPage unset( self::$mList[$name] ); } + /** + * Check if a given name exist as a special page or as a special page alias + * @param $name string: name of a special page + * @return boolean: true if a special page exists with this name + */ + static function exists( $name ) { + global $wgContLang; + if ( !self::$mListInitialised ) { + self::initList(); + } + if( !self::$mAliases ) { + self::initAliasList(); + } + + # Remove special pages inline parameters: + $bits = explode( '/', $name ); + $name = $wgContLang->caseFold($bits[0]); + + return + array_key_exists( $name, self::$mList ) + or array_key_exists( $name, self::$mAliases ) + ; + } + /** * Find the object with a given name and return it (or NULL) * @static @@ -798,4 +824,4 @@ class SpecialMycontributions extends UnlistedSpecialPage { } } -?> + diff --git a/includes/SpecialPopularpages.php b/includes/SpecialPopularpages.php index cd2f60e7..af0ed269 100644 --- a/includes/SpecialPopularpages.php +++ b/includes/SpecialPopularpages.php @@ -66,4 +66,4 @@ function wfSpecialPopularpages() { return $ppp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialPreferences.php b/includes/SpecialPreferences.php index 5ca818cd..a36be289 100644 --- a/includes/SpecialPreferences.php +++ b/includes/SpecialPreferences.php @@ -97,6 +97,8 @@ class PreferencesForm { if ( !preg_match( '/^[a-z\-]*$/', $this->mUserLanguage ) ) { $this->mUserLanguage = 'nolanguage'; } + + wfRunHooks( "InitPreferencesForm", array( $this, $request ) ); } function execute() { @@ -211,19 +213,23 @@ class PreferencesForm { if ( '' != $this->mNewpass && $wgAuth->allowPasswordChange() ) { if ( $this->mNewpass != $this->mRetypePass ) { + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'badretype' ) ); $this->mainPrefsForm( 'error', wfMsg( 'badretype' ) ); return; } if (!$wgUser->checkPassword( $this->mOldpass )) { + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'wrongpassword' ) ); $this->mainPrefsForm( 'error', wfMsg( 'wrongpassword' ) ); return; } try { $wgUser->setPassword( $this->mNewpass ); + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'success' ) ); $this->mNewpass = $this->mOldpass = $this->mRetypePass = ''; } catch( PasswordError $e ) { + wfRunHooks( "PrefsPasswordAudit", array( $wgUser, $this->mNewpass, 'error' ) ); $this->mainPrefsForm( 'error', $e->getMessage() ); return; } @@ -237,11 +243,18 @@ class PreferencesForm { } # Validate the signature and clean it up as needed - if( $this->mToggles['fancysig'] ) { + global $wgMaxSigChars; + if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) { + global $wgLang; + $this->mainPrefsForm( 'error', + wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) ); + return; + } elseif( $this->mToggles['fancysig'] ) { if( Parser::validateSig( $this->mNick ) !== false ) { $this->mNick = $wgParser->cleanSig( $this->mNick ); } else { $this->mainPrefsForm( 'error', wfMsg( 'badsig' ) ); + return; } } else { // When no fancy sig used, make sure ~{3,5} get removed. @@ -287,9 +300,17 @@ class PreferencesForm { $wgUser->setOption( $tname, $tvalue ); } if (!$wgAuth->updateExternalDB($wgUser)) { - $this->mainPrefsForm( wfMsg( 'externaldberror' ) ); + $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) ); return; } + + $msg = ''; + if ( !wfRunHooks( "SavePreferences", array( $this, $wgUser, &$msg ) ) ) { + print "(($msg))"; + $this->mainPrefsForm( 'error', $msg ); + return; + } + $wgUser->setCookies(); $wgUser->saveSettings(); @@ -321,6 +342,9 @@ class PreferencesForm { $wgUser->setCookies(); $wgUser->saveSettings(); } + if( $oldadr != $newadr ) { + wfRunHooks( "PrefsEmailAudit", array( $wgUser, $oldadr, $newadr ) ); + } } if( $needRedirect && $error === false ) { @@ -381,6 +405,8 @@ class PreferencesForm { $this->mSearchNs[$i] = $wgUser->getOption( 'searchNs'.$i ); } } + + wfRunHooks( "ResetPreferences", array( $this, $wgUser ) ); } /** @@ -441,6 +467,38 @@ class PreferencesForm { return ""; } + /** + * Helper function for user information panel + * @param $td1 label for an item + * @param $td2 item or null + * @param $td3 optional help or null + * @return xhtml block + */ + function tableRow( $td1, $td2 = null, $td3 = null ) { + global $wgContLang; + + $align['align'] = $wgContLang->isRtl() ? 'right' : 'left'; + + if ( is_null( $td3 ) ) { + $td3 = ''; + } else { + $td3 = Xml::tags( 'tr', null, + Xml::tags( 'td', array( 'colspan' => '2' ), $td3 ) + ); + } + + if ( is_null( $td2 ) ) { + $td1 = Xml::tags( 'td', $align + array( 'colspan' => '2' ), $td1 ); + $td2 = ''; + } else { + $td1 = Xml::tags( 'td', $align, $td1 ); + $td2 = Xml::tags( 'td', $align, $td2 ); + } + + return Xml::tags( 'tr', null, $td1 . $td2 ). $td3 . "\n"; + + } + /** * @access private */ @@ -457,6 +515,8 @@ class PreferencesForm { $wgOut->setArticleRelated( false ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc. + if ( $this->mSuccess || 'success' == $status ) { $wgOut->addWikitext( '
    '. wfMsg( 'savedprefs' ) . '
    ' ); } else if ( 'error' == $status ) { @@ -484,12 +544,7 @@ class PreferencesForm { $this->mUsedToggles[ 'ccmeonemails' ] = true; $this->mUsedToggles[ 'uselivepreview' ] = true; - # Enotif - # - $this->mUserEmail = htmlspecialchars( $this->mUserEmail ); - $this->mRealName = htmlspecialchars( $this->mRealName ); - $rawNick = $this->mNick; - $this->mNick = htmlspecialchars( $this->mNick ); + if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; } else { $emfc = ''; } @@ -503,7 +558,7 @@ class PreferencesForm { $skin = $wgUser->getSkin(); $emailauthenticated = wfMsg('emailnotauthenticated').'
    ' . $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ), - wfMsg( 'emailconfirmlink' ) ); + wfMsg( 'emailconfirmlink' ) ) . '
    '; } } else { $emailauthenticated = ''; @@ -511,7 +566,7 @@ class PreferencesForm { } if ($this->mUserEmail == '') { - $emailauthenticated = wfMsg( 'noemailprefs' ); + $emailauthenticated = wfMsg( 'noemailprefs' ) . '
    '; } $ps = $this->namespacesCheckboxes(); @@ -527,93 +582,85 @@ class PreferencesForm { $wgOut->addHTML( "
    " ); # User data - # - $wgOut->addHTML( "
    \n" . wfMsg('prefs-personal') . "\n
    ' . Xml::label( wfMsg( 'namespace' ), 'namespace' ) . '' . Xml::namespaceSelector( $this->namespace ) . '
    ' . Xml::namespaceSelector( $this->namespace, 'all' ) . '
    ' . Xml::label( wfMsg( 'newpages-username' ), 'mw-np-username' ) . '' . Xml::input( 'username', 30, $this->username, array( 'id' => 'mw-np-username' ) ) . '
    $td1$td2
    \n"); + $wgOut->addHTML( + Xml::openElement( 'fieldset ' ) . + Xml::element( 'legend', null, wfMsg('prefs-personal') ) . + Xml::openElement( 'table' ) . + $this->tableRow( Xml::element( 'h2', null, wfMsg( 'prefs-personal' ) ) ) + ); $userInformationHtml = - $this->addRow( - wfMsg( 'username'), - $wgUser->getName() - ) . - $this->addRow( - wfMsg( 'uid' ), - $wgUser->getID() + $this->tableRow( wfMsgHtml( 'username' ), htmlspecialchars( $wgUser->getName() ) ) . + $this->tableRow( wfMsgHtml( 'uid' ), htmlspecialchars( $wgUser->getID() ) ) . + $this->tableRow( + wfMsgHtml( 'prefs-edits' ), + $wgLang->formatNum( User::edits( $wgUser->getId() ) ) ); - + if( wfRunHooks( 'PreferencesUserInformationPanel', array( $this, &$userInformationHtml ) ) ) { $wgOut->addHtml( $userInformationHtml ); } - - if ($wgAllowRealName) { + if ( $wgAllowRealName ) { $wgOut->addHTML( - $this->addRow( - '', - "mRealName}\" size='25' />" + $this->tableRow( + Xml::label( wfMsg('yourrealname'), 'wpRealName' ), + Xml::input( 'wpRealName', 25, $this->mRealName, array( 'id' => 'wpRealName' ) ), + Xml::tags('div', array( 'class' => 'prefsectiontip' ), + wfMsgExt( 'prefs-help-realname', 'parseinline' ) + ) ) ); } - if ($wgEnableEmail) { + if ( $wgEnableEmail ) { $wgOut->addHTML( - $this->addRow( - '', - "mUserEmail}\" size='25' />" + $this->tableRow( + Xml::label( wfMsg('youremail'), 'wpUserEmail' ), + Xml::input( 'wpUserEmail', 25, $this->mUserEmail, array( 'id' => 'wpUserEmail' ) ), + Xml::tags('div', array( 'class' => 'prefsectiontip' ), + wfMsgExt( 'prefs-help-email', 'parseinline' ) + ) ) ); } - global $wgParser; - if( !empty( $this->mToggles['fancysig'] ) && - false === $wgParser->validateSig( $rawNick ) ) { - $invalidSig = $this->addRow( + global $wgParser, $wgMaxSigChars; + if( mb_strlen( $this->mNick ) > $wgMaxSigChars ) { + $invalidSig = $this->tableRow( + ' ', + Xml::element( 'span', array( 'class' => 'error' ), + wfMsg( 'badsiglength', $wgLang->formatNum( $wgMaxSigChars ) ) ) + ); + } elseif( !empty( $this->mToggles['fancysig'] ) && + false === $wgParser->validateSig( $this->mNick ) ) { + $invalidSig = $this->tableRow( ' ', - '' . wfMsgHtml( 'badsig' ) . '' + Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) ) ); } else { $invalidSig = ''; } $wgOut->addHTML( - $this->addRow( - '', - "mNick}\" size='25' />" + $this->tableRow( + Xml::label( wfMsg( 'yournick' ), 'wpNick' ), + Xml::input( 'wpNick', 25, $this->mNick, + array( + 'id' => 'wpNick', + // Note: $wgMaxSigChars is enforced in Unicode characters, + // both on the backend and now in the browser. + // Badly-behaved requests may still try to submit + // an overlong string, however. + 'maxlength' => $wgMaxSigChars ) ) ) . $invalidSig . - # FIXME: The part should be where the   is, getToggle() needs - # to be changed to out return its output in two parts. -ævar - $this->addRow( - ' ', - $this->getToggle( 'fancysig' ) - ) + $this->tableRow( ' ', $this->getToggle( 'fancysig' ) ) ); - /** - * Make sure the site language is in the list; a custom language code - * might not have a defined name... - */ - $languages = Language::getLanguageNames( true ); - if( !array_key_exists( $wgContLanguageCode, $languages ) ) { - $languages[$wgContLanguageCode] = $wgContLanguageCode; - } - ksort( $languages ); - - /** - * If a bogus value is set, default to the content language. - * Otherwise, no default is selected and the user ends up - * with an Afrikaans interface since it's first in the list. - */ - $selectedLang = isset( $languages[$this->mUserLanguage] ) ? $this->mUserLanguage : $wgContLanguageCode; - $options = "\n"; - foreach( $languages as $code => $name ) { - $selected = ($code == $selectedLang); - $options .= Xml::option( "$code - $name", $code, $selected ) . "\n"; - } + list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage ); $wgOut->addHTML( - $this->addRow( - '', - "" - ) + $this->tableRow( $lsLabel, $lsSelect ) ); /* see if there are multiple language variants to choose from*/ @@ -621,6 +668,7 @@ class PreferencesForm { $variants = $wgContLang->getVariants(); $variantArray = array(); + $languages = Language::getLanguageNames( true ); foreach($variants as $v) { $v = str_replace( '_', '-', strtolower($v)); if( array_key_exists( $v, $languages ) ) { @@ -637,69 +685,74 @@ class PreferencesForm { if(count($variantArray) > 1) { $wgOut->addHtml( - $this->addRow( wfMsg( 'yourvariant' ), - "" ) + $this->tableRow( + Xml::label( wfMsg( 'yourvariant' ), 'wpUserVariant' ), + Xml::tags( 'select', + array( 'name' => 'wpUserVariant', 'id' => 'wpUserVariant' ), + $options + ) + ) ); } } - $wgOut->addHTML('
    '); # Password - if( $wgAuth->allowPasswordChange() ) { - $this->mOldpass = htmlspecialchars( $this->mOldpass ); - $this->mNewpass = htmlspecialchars( $this->mNewpass ); - $this->mRetypePass = htmlspecialchars( $this->mRetypePass ); - - $wgOut->addHTML( '
    ' . wfMsg( 'changepassword' ) . ''); + if( $wgAuth->allowPasswordChange() ) { $wgOut->addHTML( - $this->addRow( - '', - "mOldpass}\" size='20' />" + $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) . + $this->tableRow( + Xml::label( wfMsg( 'oldpassword' ), 'wpOldpass' ), + Xml::password( 'wpOldpass', 25, $this->mOldpass, array( 'id' => 'wpOldpass' ) ) ) . - $this->addRow( - '', - "mNewpass}\" size='20' />" + $this->tableRow( + Xml::label( wfMsg( 'newpassword' ), 'wpNewpass' ), + Xml::password( 'wpNewpass', 25, $this->mNewpass, array( 'id' => 'wpNewpass' ) ) ) . - $this->addRow( - '', - "mRetypePass}\" size='20' />" + $this->tableRow( + Xml::label( wfMsg( 'retypenew' ), 'wpRetypePass' ), + Xml::password( 'wpRetypePass', 25, $this->mRetypePass, array( 'id' => 'wpRetypePass' ) ) ) . - "
    \n" . - $this->getToggle( "rememberpassword" ) . "
    \n\n" ); + Xml::tags( 'tr', null, + Xml::tags( 'td', array( 'colspan' => '2' ), + $this->getToggle( "rememberpassword" ) + ) + ) + ); } # # Enotif - if ($wgEnableEmail) { - $wgOut->addHTML( '
    ' . wfMsg( 'email' ) . '' ); + if ( $wgEnableEmail ) { + + $moreEmail = ''; + if ($wgEnableUserEmail) { + $emf = wfMsg( 'allowemail' ); + $disabled = $disableEmailPrefs ? ' disabled="disabled"' : ''; + $moreEmail = + " "; + } + + $wgOut->addHTML( + $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) . + $this->tableRow( $emailauthenticated. $enotifrevealaddr. $enotifwatchlistpages. $enotifusertalkpages. - $enotifminoredits ); - if ($wgEnableUserEmail) { - $emf = wfMsg( 'allowemail' ); - $disabled = $disableEmailPrefs ? ' disabled="disabled"' : ''; - $wgOut->addHTML( - "
    " ); - } - $wgOut->addHtml( $this->getToggle( 'ccmeonemails' ) ); - - $wgOut->addHTML( '
    ' ); + $enotifminoredits. + $moreEmail. + $this->getToggle( 'ccmeonemails' ) + ) + ); } #
    - # Show little "help" tips for the real name and email address options - if( $wgAllowRealName || $wgEnableEmail ) { - if( $wgAllowRealName ) - $tips[] = wfMsg( 'prefs-help-realname' ); - if( $wgEnableEmail ) - $tips[] = wfMsg( 'prefs-help-email' ); - $wgOut->addHtml( '
    ' . implode( '
    ', $tips ) . '
    ' ); - } + $wgOut->addHTML( + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ) + ); - $wgOut->addHTML( '' ); # Quickbar # @@ -753,8 +806,12 @@ class PreferencesForm { if( $wgUseTeX ) { $wgOut->addHTML( "
    \n" . wfMsg('math') . '' ); foreach ( $mathopts as $k => $v ) { - $checked = $k == $this->mMath ? ' checked="checked"' : ''; - $wgOut->addHTML( "
    \n" ); + $checked = ($k == $this->mMath); + $wgOut->addHTML( + Xml::openElement( 'div' ) . + Xml::radioLabel( wfMsg( $v ), 'wpMath', $k, "mw-sp-math-$k", $checked ) . + Xml::closeElement( 'div' ) . "\n" + ); } $wgOut->addHTML( "
    \n\n" ); } @@ -928,8 +985,8 @@ class PreferencesForm { # Misc # $wgOut->addHTML('
    ' . wfMsg('prefs-misc') . ''); - $wgOut->addHTML( wfInputLabel( wfMsg( 'stubthreshold' ), - 'wpStubs', 'wpStubs', 6, $this->mStubs ) ); + $wgOut->addHtml( ' ' ); + $wgOut->addHtml( Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) ); $msgUnderline = htmlspecialchars( wfMsg ( 'tog-underline' ) ); $msgUnderlinenever = htmlspecialchars( wfMsg ( 'underline-never' ) ); $msgUnderlinealways = htmlspecialchars( wfMsg ( 'underline-always' ) ); @@ -953,7 +1010,9 @@ class PreferencesForm { } $wgOut->addHTML( '
    ' ); - $token = $wgUser->editToken(); + wfRunHooks( "RenderPreferencesForm", array( $this, $wgOut ) ); + + $token = htmlspecialchars( $wgUser->editToken() ); $skin = $wgUser->getSkin(); $wgOut->addHTML( "
    @@ -964,11 +1023,13 @@ class PreferencesForm {
    - + \n" ); - $wgOut->addWikiText( '
    ' . wfMsg('clearyourcache') . '
    ' ); + $wgOut->addHtml( Xml::tags( 'div', array( 'class' => "prefcache" ), + wfMsgExt( 'clearyourcache', 'parseinline' ) ) + ); } } -?> + diff --git a/includes/SpecialPrefixindex.php b/includes/SpecialPrefixindex.php index b7c51d49..6bb26d67 100644 --- a/includes/SpecialPrefixindex.php +++ b/includes/SpecialPrefixindex.php @@ -15,20 +15,14 @@ function wfSpecialPrefixIndex( $par=NULL, $specialPage ) { $from = $wgRequest->getVal( 'from' ); $prefix = $wgRequest->getVal( 'prefix' ); $namespace = $wgRequest->getInt( 'namespace' ); - $namespaces = $wgContLang->getNamespaces(); $indexPage = new SpecialPrefixIndex(); - if( !in_array($namespace, array_keys($namespaces)) ) - $namespace = 0; - - $wgOut->setPagetitle( $namespace > 0 ? - wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) : - wfMsg( 'allarticles' ) - ); - - + $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces ) ) ) + ? wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) + : wfMsg( 'allarticles' ) + ); if ( isset($par) ) { $indexPage->showChunk( $namespace, $par, $specialPage->including(), $from ); @@ -67,9 +61,15 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n $fromList = $this->getNamespaceKeyAndText($namespace, $from); $prefixList = $this->getNamespaceKeyAndText($namespace, $prefix); + $namespaces = $wgContLang->getNamespaces(); + $align = $wgContLang->isRtl() ? 'left' : 'right'; if ( !$prefixList || !$fromList ) { $out = wfMsgWikiHtml( 'allpagesbadtitle' ); + } elseif ( !in_array( $namespace, array_keys( $namespaces ) ) ) { + // Show errormessage and reset to NS_MAIN + $out = wfMsgExt( 'allpages-bad-ns', array( 'parseinline' ), $namespace ); + $namespace = NS_MAIN; } else { list( $namespace, $prefixKey, $prefix ) = $prefixList; list( /* $fromNs */, $fromKey, $from ) = $fromList; @@ -127,8 +127,8 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n } else { $nsForm = $this->namespaceForm ( $namespace, $prefix ); $out2 = ''; - $out2 .= ' + + + + + + + + + + " . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ); + $wgOut->addHtml( $table ); } - + $wgOut->addHTML( "

    " . htmlspecialchars( wfMsg( "history" ) ) . "

    \n" ); if( $haveRevisions ) { @@ -763,7 +795,7 @@ class UndeleteForm { while( $row = $revisions->fetchObject() ) { $ts = wfTimestamp( TS_MW, $row->ar_timestamp ); if ( $this->mAllowed ) { - $checkBox = wfCheck( "ts$ts" ); + $checkBox = Xml::check( "ts$ts" ); $pageLink = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target×tamp=$ts" ); @@ -782,7 +814,7 @@ class UndeleteForm { } $comment = $sk->commentBlock( $row->ar_comment ); $wgOut->addHTML( "
  • $checkBox $pageLink . . $userLink $stxt $comment
  • \n" ); - + } $revisions->free(); $wgOut->addHTML(""); @@ -790,14 +822,13 @@ class UndeleteForm { $wgOut->addWikiText( wfMsg( "nohistory" ) ); } - if( $haveFiles ) { - $wgOut->addHtml( "

    " . wfMsgHtml( 'imghistory' ) . "

    \n" ); + $wgOut->addHtml( "

    " . wfMsgHtml( 'filehist' ) . "

    \n" ); $wgOut->addHtml( "
      " ); while( $row = $files->fetchObject() ) { $ts = wfTimestamp( TS_MW, $row->fa_timestamp ); if ( $this->mAllowed && $row->fa_storage_key ) { - $checkBox = wfCheck( "fileid" . $row->fa_id ); + $checkBox = Xml::check( "fileid" . $row->fa_id ); $key = urlencode( $row->fa_storage_key ); $target = urlencode( $this->mTarget ); $pageLink = $sk->makeKnownLinkObj( $titleObj, @@ -821,12 +852,13 @@ class UndeleteForm { $files->free(); $wgOut->addHTML( "
    " ); } - + if ( $this->mAllowed ) { # Slip in the hidden controls here - $misc = wfHidden( 'target', $this->mTarget ); - $misc .= wfHidden( 'wpEditToken', $wgUser->editToken() ); - $wgOut->addHtml( $misc . '' ); + $misc = Xml::hidden( 'target', $this->mTarget ); + $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() ); + $misc .= Xml::closeElement( 'form' ); + $wgOut->addHtml( $misc ); } return true; @@ -841,17 +873,25 @@ class UndeleteForm { $this->mTargetTimestamp, $this->mComment, $this->mFileVersions ); - + if( $ok ) { $skin = $wgUser->getSkin(); $link = $skin->makeKnownLinkObj( $this->mTargetObj ); $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) ); - return true; + } else { + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); + } + + // Show file deletion warnings and errors + $status = $archive->getFileStatus(); + if ( $status && !$status->isGood() ) { + $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) ); } + } else { + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); } - $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); return false; } } -?> + diff --git a/includes/SpecialUnlockdb.php b/includes/SpecialUnlockdb.php index e864a182..52025e53 100644 --- a/includes/SpecialUnlockdb.php +++ b/includes/SpecialUnlockdb.php @@ -107,4 +107,4 @@ END } } -?> + diff --git a/includes/SpecialUnusedcategories.php b/includes/SpecialUnusedcategories.php index 5cd3406b..492c5f84 100644 --- a/includes/SpecialUnusedcategories.php +++ b/includes/SpecialUnusedcategories.php @@ -15,7 +15,7 @@ class UnusedCategoriesPage extends QueryPage { } function getPageHeader() { - return '

    ' . wfMsg('unusedcategoriestext') . '

    '; + return wfMsgExt( 'unusedcategoriestext', array( 'parse' ) ); } function getSQL() { @@ -43,4 +43,4 @@ function wfSpecialUnusedCategories() { $uc = new UnusedCategoriesPage(); return $uc->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUnusedimages.php b/includes/SpecialUnusedimages.php index 6b99192a..52aa19d2 100644 --- a/includes/SpecialUnusedimages.php +++ b/includes/SpecialUnusedimages.php @@ -54,4 +54,4 @@ function wfSpecialUnusedimages() { return $uip->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUnusedtemplates.php b/includes/SpecialUnusedtemplates.php index 8b72e8a7..79e99f3a 100644 --- a/includes/SpecialUnusedtemplates.php +++ b/includes/SpecialUnusedtemplates.php @@ -37,8 +37,7 @@ class UnusedtemplatesPage extends QueryPage { } function getPageHeader() { - global $wgOut; - return $wgOut->parse( wfMsg( 'unusedtemplatestext' ) ); + return wfMsgExt( 'unusedtemplatestext', array( 'parse' ) ); } } @@ -49,4 +48,4 @@ function wfSpecialUnusedtemplates() { $utp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUnwatchedpages.php b/includes/SpecialUnwatchedpages.php index fed0b590..b1883e97 100644 --- a/includes/SpecialUnwatchedpages.php +++ b/includes/SpecialUnwatchedpages.php @@ -62,4 +62,4 @@ function wfSpecialUnwatchedpages() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUpload.php b/includes/SpecialUpload.php index e07c414c..18c6dd9e 100644 --- a/includes/SpecialUpload.php +++ b/includes/SpecialUpload.php @@ -22,17 +22,19 @@ class UploadForm { /**#@+ * @access private */ - var $mUploadFile, $mUploadDescription, $mLicense ,$mIgnoreWarning, $mUploadError; - var $mUploadSaveName, $mUploadTempName, $mUploadSize, $mUploadOldVersion; - var $mUploadCopyStatus, $mUploadSource, $mReUpload, $mAction, $mUpload; - var $mOname, $mSessionKey, $mStashed, $mDestFile, $mRemoveTempFile, $mSourceType; - var $mUploadTempFileSize = 0; + var $mComment, $mLicense, $mIgnoreWarning, $mCurlError; + var $mDestName, $mTempPath, $mFileSize, $mFileProps; + var $mCopyrightStatus, $mCopyrightSource, $mReUpload, $mAction, $mUploadClicked; + var $mSrcName, $mSessionKey, $mStashed, $mDesiredDestName, $mRemoveTempFile, $mSourceType; + var $mDestWarningAck, $mCurlDestHandle; + var $mLocalFile; # Placeholders for text injection by hooks (must be HTML) # extensions should take care to _append_ to the present value var $uploadFormTextTop; var $uploadFormTextAfterSummary; + const SESSION_VERSION = 1; /**#@-*/ /** @@ -42,10 +44,13 @@ class UploadForm { */ function UploadForm( &$request ) { global $wgAllowCopyUploads; - $this->mDestFile = $request->getText( 'wpDestFile' ); + $this->mDesiredDestName = $request->getText( 'wpDestFile' ); + $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); + $this->mComment = $request->getText( 'wpUploadDescription' ); if( !$request->wasPosted() ) { - # GET requests just give the main form; no data except wpDestfile. + # GET requests just give the main form; no data except destination + # filename and description return; } @@ -53,23 +58,22 @@ class UploadForm { $this->uploadFormTextTop = ""; $this->uploadFormTextAfterSummary = ""; - $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' ); $this->mReUpload = $request->getCheck( 'wpReUpload' ); - $this->mUpload = $request->getCheck( 'wpUpload' ); + $this->mUploadClicked = $request->getCheck( 'wpUpload' ); - $this->mUploadDescription = $request->getText( 'wpUploadDescription' ); $this->mLicense = $request->getText( 'wpLicense' ); - $this->mUploadCopyStatus = $request->getText( 'wpUploadCopyStatus' ); - $this->mUploadSource = $request->getText( 'wpUploadSource' ); + $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' ); + $this->mCopyrightSource = $request->getText( 'wpUploadSource' ); $this->mWatchthis = $request->getBool( 'wpWatchthis' ); - $this->mSourceType = $request->getText( 'wpSourceType' ); - wfDebug( "UploadForm: watchthis is: '$this->mWatchthis'\n" ); + $this->mSourceType = $request->getText( 'wpSourceType' ); + $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' ); $this->mAction = $request->getVal( 'action' ); $this->mSessionKey = $request->getInt( 'wpSessionKey' ); if( !empty( $this->mSessionKey ) && - isset( $_SESSION['wsUploadData'][$this->mSessionKey] ) ) { + isset( $_SESSION['wsUploadData'][$this->mSessionKey]['version'] ) && + $_SESSION['wsUploadData'][$this->mSessionKey]['version'] == self::SESSION_VERSION ) { /** * Confirming a temporarily stashed upload. * We don't want path names to be forged, so we keep @@ -77,10 +81,11 @@ class UploadForm { * an opaque key to the user agent. */ $data = $_SESSION['wsUploadData'][$this->mSessionKey]; - $this->mUploadTempName = $data['mUploadTempName']; - $this->mUploadSize = $data['mUploadSize']; - $this->mOname = $data['mOname']; - $this->mUploadError = 0/*UPLOAD_ERR_OK*/; + $this->mTempPath = $data['mTempPath']; + $this->mFileSize = $data['mFileSize']; + $this->mSrcName = $data['mSrcName']; + $this->mFileProps = $data['mFileProps']; + $this->mCurlError = 0/*UPLOAD_ERR_OK*/; $this->mStashed = true; $this->mRemoveTempFile = false; } else { @@ -100,10 +105,10 @@ class UploadForm { * @access private */ function initializeFromUpload( $request ) { - $this->mUploadTempName = $request->getFileTempName( 'wpUploadFile' ); - $this->mUploadSize = $request->getFileSize( 'wpUploadFile' ); - $this->mOname = $request->getFileName( 'wpUploadFile' ); - $this->mUploadError = $request->getUploadError( 'wpUploadFile' ); + $this->mTempPath = $request->getFileTempName( 'wpUploadFile' ); + $this->mFileSize = $request->getFileSize( 'wpUploadFile' ); + $this->mSrcName = $request->getFileName( 'wpUploadFile' ); + $this->mCurlError = $request->getUploadError( 'wpUploadFile' ); $this->mSessionKey = false; $this->mStashed = false; $this->mRemoveTempFile = false; // PHP will handle this @@ -118,10 +123,10 @@ class UploadForm { $url = $request->getText( 'wpUploadFileURL' ); $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); - $this->mUploadTempName = $local_file; - $this->mUploadError = $this->curlCopy( $url, $local_file ); - $this->mUploadSize = $this->mUploadTempFileSize; - $this->mOname = array_pop( explode( '/', $url ) ); + $this->mTempPath = $local_file; + $this->mFileSize = 0; # Will be set by curlCopy + $this->mCurlError = $this->curlCopy( $url, $local_file ); + $this->mSrcName = array_pop( explode( '/', $url ) ); $this->mSessionKey = false; $this->mStashed = false; @@ -150,8 +155,8 @@ class UploadForm { } # Open temporary file - $this->mUploadTempFile = @fopen( $this->mUploadTempName, "wb" ); - if( $this->mUploadTempFile === false ) { + $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" ); + if( $this->mCurlDestHandle === false ) { # Could not open temporary file to write in $wgOut->errorPage( 'upload-file-error', 'upload-file-error-text'); return true; @@ -169,8 +174,8 @@ class UploadForm { // if ( $error ) print curl_error ( $ch ) ; # Debugging output curl_close( $ch ); - fclose( $this->mUploadTempFile ); - unset( $this->mUploadTempFile ); + fclose( $this->mCurlDestHandle ); + unset( $this->mCurlDestHandle ); if( $error ) { unlink( $dest ); if( wfEmptyMsg( "upload-curl-error$errornum", wfMsg("upload-curl-error$errornum") ) ) @@ -191,11 +196,11 @@ class UploadForm { function uploadCurlCallback( $ch, $data ) { global $wgMaxUploadSize; $length = strlen( $data ); - $this->mUploadTempFileSize += $length; - if( $this->mUploadTempFileSize > $wgMaxUploadSize ) { + $this->mFileSize += $length; + if( $this->mFileSize > $wgMaxUploadSize ) { return 0; } - fwrite( $this->mUploadTempFile, $data ); + fwrite( $this->mCurlDestHandle, $data ); return $length; } @@ -205,11 +210,11 @@ class UploadForm { */ function execute() { global $wgUser, $wgOut; - global $wgEnableUploads, $wgUploadDirectory; + global $wgEnableUploads; # Check uploading enabled if( !$wgEnableUploads ) { - $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' ); + $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) ); return; } @@ -234,18 +239,12 @@ class UploadForm { return; } - /** Check if the image directory is writeable, this is a common mistake */ - if( !is_writeable( $wgUploadDirectory ) ) { - $wgOut->addWikiText( wfMsg( 'upload_directory_read_only', $wgUploadDirectory ) ); - return; - } - if( $this->mReUpload ) { if( !$this->unsaveUploadedFile() ) { return; } $this->mainUploadForm(); - } else if( 'submit' == $this->mAction || $this->mUpload ) { + } else if( 'submit' == $this->mAction || $this->mUploadClicked ) { $this->processUpload(); } else { $this->mainUploadForm(); @@ -271,7 +270,7 @@ class UploadForm { } /* Check for PHP error if any, requires php 4.2 or newer */ - if( $this->mUploadError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { + if( $this->mCurlError == 1/*UPLOAD_ERR_INI_SIZE*/ ) { $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); return; } @@ -279,23 +278,24 @@ class UploadForm { /** * If there was no filename or a zero size given, give up quick. */ - if( trim( $this->mOname ) == '' || empty( $this->mUploadSize ) ) { + if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) { $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) ); return; } # Chop off any directories in the given filename - if( $this->mDestFile ) { - $basename = wfBaseName( $this->mDestFile ); + if( $this->mDesiredDestName ) { + $basename = $this->mDesiredDestName; } else { - $basename = wfBaseName( $this->mOname ); + $basename = $this->mSrcName; } + $filtered = wfBaseName( $basename ); /** * We'll want to blacklist against *any* 'extension', and use * only the final one for the whitelist. */ - list( $partname, $ext ) = $this->splitExtensions( $basename ); + list( $partname, $ext ) = $this->splitExtensions( $filtered ); if( count( $ext ) ) { $finalExt = $ext[count( $ext ) - 1]; @@ -310,8 +310,8 @@ class UploadForm { $partname .= '.' . $ext[$i]; } - if( strlen( $partname ) < 3 ) { - $this->mainUploadForm( wfMsgHtml( 'minlength' ) ); + if( strlen( $partname ) < 1 ) { + $this->mainUploadForm( wfMsgHtml( 'minlength1' ) ); return; } @@ -319,14 +319,14 @@ class UploadForm { * Filter out illegal characters, and try to make a legible name * out of it. We'll strip some silently that Title would die on. */ - $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $basename ); - $nt = Title::newFromText( $filtered ); + $filtered = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $filtered ); + $nt = Title::makeTitleSafe( NS_IMAGE, $filtered ); if( is_null( $nt ) ) { $this->uploadError( wfMsgWikiHtml( 'illegalfilename', htmlspecialchars( $filtered ) ) ); return; } - $nt =& Title::makeTitle( NS_IMAGE, $nt->getDBkey() ); - $this->mUploadSaveName = $nt->getDBkey(); + $this->mLocalFile = wfLocalFile( $nt ); + $this->mDestName = $this->mLocalFile->getName(); /** * If the image is protected, non-sysop users won't be able @@ -339,7 +339,7 @@ class UploadForm { /** * In some cases we may forbid overwriting of existing files. */ - $overwrite = $this->checkOverwrite( $this->mUploadSaveName ); + $overwrite = $this->checkOverwrite( $this->mDestName ); if( WikiError::isError( $overwrite ) ) { return $this->uploadError( $overwrite->toString() ); } @@ -350,9 +350,9 @@ class UploadForm { if ($finalExt == '') { return $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) ); } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || - ($wgStrictFileExtensions && - !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { - return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) ); + ($wgStrictFileExtensions && !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { + return $this->uploadError( wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), + htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ) ); } /** @@ -361,23 +361,25 @@ class UploadForm { * probably not accept it. */ if( !$this->mStashed ) { + $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt ); $this->checkMacBinary(); - $veri = $this->verify( $this->mUploadTempName, $finalExt ); + $veri = $this->verify( $this->mTempPath, $finalExt ); if( $veri !== true ) { //it's a wiki error... return $this->uploadError( $veri->toString() ); } - } - /** - * Provide an opportunity for extensions to add futher checks - */ - $error = ''; - if( !wfRunHooks( 'UploadVerification', - array( $this->mUploadSaveName, $this->mUploadTempName, &$error ) ) ) { - return $this->uploadError( $error ); + /** + * Provide an opportunity for extensions to add further checks + */ + $error = ''; + if( !wfRunHooks( 'UploadVerification', + array( $this->mDestName, $this->mTempPath, &$error ) ) ) { + return $this->uploadError( $error ); + } } + /** * Check for non-fatal conditions */ @@ -388,97 +390,32 @@ class UploadForm { if( $wgCapitalLinks ) { $filtered = ucfirst( $filtered ); } - if( $this->mUploadSaveName != $filtered ) { - $warning .= '
  • '.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mUploadSaveName ) ).'
  • '; + if( $basename != $filtered ) { + $warning .= '
  • '.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'
  • '; } global $wgCheckFileExtensions; if ( $wgCheckFileExtensions ) { if ( ! $this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { - $warning .= '
  • '.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'
  • '; + $warning .= '
  • '.wfMsgExt( 'filetype-badtype', array ( 'parseinline' ), + htmlspecialchars( $finalExt ), implode ( ', ', $wgFileExtensions ) ).'
  • '; } } global $wgUploadSizeWarning; - if ( $wgUploadSizeWarning && ( $this->mUploadSize > $wgUploadSizeWarning ) ) { + if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { $skin = $wgUser->getSkin(); $wsize = $skin->formatSize( $wgUploadSizeWarning ); - $asize = $skin->formatSize( $this->mUploadSize ); + $asize = $skin->formatSize( $this->mFileSize ); $warning .= '
  • ' . wfMsgHtml( 'large-file', $wsize, $asize ) . '
  • '; } - if ( $this->mUploadSize == 0 ) { + if ( $this->mFileSize == 0 ) { $warning .= '
  • '.wfMsgHtml( 'emptyfile' ).'
  • '; } - global $wgUser; - $sk = $wgUser->getSkin(); - $image = new Image( $nt ); - - // Check for uppercase extension. We allow these filenames but check if an image - // with lowercase extension exists already - if ( $finalExt != strtolower( $finalExt ) ) { - $nt_lc = Title::newFromText( $partname . '.' . strtolower( $finalExt ) ); - $image_lc = new Image( $nt_lc ); + if ( !$this->mDestWarningAck ) { + $warning .= self::getExistsWarning( $this->mLocalFile ); } - - if( $image->exists() ) { - $dlink = $sk->makeKnownLinkObj( $nt ); - if ( $image->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt->getText(), 'right', array(), false, true ); - } elseif ( !$image->allowInlineDisplay() && $image->isSafeFile() ) { - $icon = $image->iconThumb(); - $dlink2 = ''; - } else { - $dlink2 = ''; - } - - $warning .= '
  • ' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '
  • ' . $dlink2; - - } elseif ( isset( $image_lc) && $image_lc->exists() ) { - # Check if image with lowercase extension exists. - # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension - $dlink = $sk->makeKnownLinkObj( $nt_lc ); - if ( $image_lc->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt_lc->getText(), 'right', array(), false, true ); - } elseif ( !$image_lc->allowInlineDisplay() && $image_lc->isSafeFile() ) { - $icon = $image_lc->iconThumb(); - $dlink2 = ''; - } else { - $dlink2 = ''; - } - - $warning .= '
  • ' . wfMsgExt( 'fileexists-extension', 'parsemag' , $partname . '.' . $finalExt , $dlink ) . '
  • ' . $dlink2; - - } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) { - # Check for filenames like 50px- or 180px-, these are mostly thumbnails - $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $finalExt ); - $image_thb = new Image( $nt_thb ); - if ($image_thb->exists() ) { - # Check if an image without leading '180px-' (or similiar) exists - $dlink = $sk->makeKnownLinkObj( $nt_thb); - if ( $image_thb->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_thb, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), $nt_thb->getText(), 'right', array(), false, true ); - } elseif ( !$image_thb->allowInlineDisplay() && $image_thb->isSafeFile() ) { - $icon = $image_thb->iconThumb(); - $dlink2 = ''; - } else { - $dlink2 = ''; - } - - $warning .= '
  • ' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . '
  • ' . $dlink2; - } else { - # Image w/o '180px-' does not exists, but we do not like these filenames - $warning .= '
  • ' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '
  • '; - } - } - if ( $image->wasDeleted() ) { - # If the file existed before and was deleted, warn the user of this - # Don't bother doing so if the image exists now, however - $ltitle = SpecialPage::getTitleFor( 'Log' ); - $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), 'type=delete&page=' . $nt->getPrefixedUrl() ); - $warning .= wfOpenElement( 'li' ) . wfMsgWikiHtml( 'filewasdeleted', $llink ) . wfCloseElement( 'li' ); - } - if( $warning != '' ) { /** * Stash the file in a temporary location; the user can choose @@ -492,91 +429,164 @@ class UploadForm { * Try actually saving the thing... * It will show an error form on failure. */ - $hasBeenMunged = !empty( $this->mSessionKey ) || $this->mRemoveTempFile; - if( $this->saveUploadedFile( $this->mUploadSaveName, - $this->mUploadTempName, - $hasBeenMunged ) ) { - /** - * Update the upload log and create the description page - * if it's a new file. - */ - $img = Image::newFromName( $this->mUploadSaveName ); - $success = $img->recordUpload( $this->mUploadOldVersion, - $this->mUploadDescription, - $this->mLicense, - $this->mUploadCopyStatus, - $this->mUploadSource, - $this->mWatchthis ); - - if ( $success ) { - $this->showSuccess(); - wfRunHooks( 'UploadComplete', array( &$img ) ); - } else { - // Image::recordUpload() fails if the image went missing, which is - // unlikely, hence the lack of a specialised message - $wgOut->showFileNotFoundError( $this->mUploadSaveName ); + $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, + $this->mCopyrightStatus, $this->mCopyrightSource ); + + $status = $this->mLocalFile->upload( $this->mTempPath, $this->mComment, $pageText, + File::DELETE_SOURCE, $this->mFileProps ); + if ( !$status->isGood() ) { + $this->showError( $status->getWikiText() ); + } else { + if ( $this->mWatchthis ) { + global $wgUser; + $wgUser->addWatch( $this->mLocalFile->getTitle() ); } + // Success, redirect to description page + $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() ); + $img = null; // @todo: added to avoid passing a ref to null - should this be defined somewhere? + wfRunHooks( 'UploadComplete', array( &$img ) ); } } /** - * Move the uploaded file from its temporary location to the final - * destination. If a previous version of the file exists, move - * it into the archive subdirectory. - * - * @todo If the later save fails, we may have disappeared the original file. - * - * @param string $saveName - * @param string $tempName full path to the temporary file - * @param bool $useRename if true, doesn't check that the source file - * is a PHP-managed upload temporary + * Do existence checks on a file and produce a warning + * This check is static and can be done pre-upload via AJAX + * Returns an HTML fragment consisting of one or more LI elements if there is a warning + * Returns an empty string if there is no warning */ - function saveUploadedFile( $saveName, $tempName, $useRename = false ) { - global $wgOut, $wgAllowCopyUploads; + static function getExistsWarning( $file ) { + global $wgUser; + // Check for uppercase extension. We allow these filenames but check if an image + // with lowercase extension exists already + $warning = ''; + + if( strpos( $file->getName(), '.' ) == false ) { + $partname = $file->getName(); + $rawExtension = ''; + } else { + list( $partname, $rawExtension ) = explode( '.', $file->getName(), 2 ); + } + $sk = $wgUser->getSkin(); - if ( !$useRename AND $wgAllowCopyUploads AND $this->mSourceType == 'web' ) $useRename = true; + if ( $rawExtension != $file->getExtension() ) { + // We're not using the normalized form of the extension. + // Normal form is lowercase, using most common of alternate + // extensions (eg 'jpg' rather than 'JPEG'). + // + // Check for another file using the normalized form... + $nt_lc = Title::newFromText( $partname . '.' . $file->getExtension() ); + $file_lc = wfLocalFile( $nt_lc ); + } else { + $file_lc = false; + } - $fname= "SpecialUpload::saveUploadedFile"; + if( $file->exists() ) { + $dlink = $sk->makeKnownLinkObj( $file->getTitle() ); + if ( $file->allowInlineDisplay() ) { + $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), + $file->getName(), 'right', array(), false, true ); + } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) { + $icon = $file->iconThumb(); + $dlink2 = '
    ' . + $icon->toHtml( array( 'desc-link' => true ) ) . '
    ' . $dlink . '
    '; + } else { + $dlink2 = ''; + } - $dest = wfImageDir( $saveName ); - $archive = wfImageArchiveDir( $saveName ); - if ( !is_dir( $dest ) ) wfMkdirParents( $dest ); - if ( !is_dir( $archive ) ) wfMkdirParents( $archive ); + $warning .= '
  • ' . wfMsgExt( 'fileexists', 'parseline', $dlink ) . '
  • ' . $dlink2; + + } elseif ( $file_lc && $file_lc->exists() ) { + # Check if image with lowercase extension exists. + # It's not forbidden but in 99% it makes no sense to upload the same filename with uppercase extension + $dlink = $sk->makeKnownLinkObj( $nt_lc ); + if ( $file_lc->allowInlineDisplay() ) { + $dlink2 = $sk->makeImageLinkObj( $nt_lc, wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), + $nt_lc->getText(), 'right', array(), false, true ); + } elseif ( !$file_lc->allowInlineDisplay() && $file_lc->isSafeFile() ) { + $icon = $file_lc->iconThumb(); + $dlink2 = '
    ' . + $icon->toHtml( array( 'desc-link' => true ) ) . '
    ' . $dlink . '
    '; + } else { + $dlink2 = ''; + } - $this->mSavedFile = "{$dest}/{$saveName}"; + $warning .= '
  • ' . wfMsgExt( 'fileexists-extension', 'parsemag', $file->getName(), $dlink ) . '
  • ' . $dlink2; - if( is_file( $this->mSavedFile ) ) { - $this->mUploadOldVersion = gmdate( 'YmdHis' ) . "!{$saveName}"; - wfSuppressWarnings(); - $success = rename( $this->mSavedFile, "${archive}/{$this->mUploadOldVersion}" ); - wfRestoreWarnings(); + } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) + && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) + { + # Check for filenames like 50px- or 180px-, these are mostly thumbnails + $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension ); + $file_thb = wfLocalFile( $nt_thb ); + if ($file_thb->exists() ) { + # Check if an image without leading '180px-' (or similiar) exists + $dlink = $sk->makeKnownLinkObj( $nt_thb); + if ( $file_thb->allowInlineDisplay() ) { + $dlink2 = $sk->makeImageLinkObj( $nt_thb, + wfMsgExt( 'fileexists-thumb', 'parseinline', $dlink ), + $nt_thb->getText(), 'right', array(), false, true ); + } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) { + $icon = $file_thb->iconThumb(); + $dlink2 = '
    ' . + $icon->toHtml( array( 'desc-link' => true ) ) . '
    ' . + $dlink . '
    '; + } else { + $dlink2 = ''; + } - if( ! $success ) { - $wgOut->showFileRenameError( $this->mSavedFile, - "${archive}/{$this->mUploadOldVersion}" ); - return false; + $warning .= '
  • ' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . + '
  • ' . $dlink2; + } else { + # Image w/o '180px-' does not exists, but we do not like these filenames + $warning .= '
  • ' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , + substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '
  • '; } - else wfDebug("$fname: moved file ".$this->mSavedFile." to ${archive}/{$this->mUploadOldVersion}\n"); } - else { - $this->mUploadOldVersion = ''; + if ( $file->wasDeleted() ) { + # If the file existed before and was deleted, warn the user of this + # Don't bother doing so if the image exists now, however + $ltitle = SpecialPage::getTitleFor( 'Log' ); + $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), + 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() ); + $warning .= '
  • ' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '
  • '; } + return $warning; + } - wfSuppressWarnings(); - $success = $useRename - ? rename( $tempName, $this->mSavedFile ) - : move_uploaded_file( $tempName, $this->mSavedFile ); - wfRestoreWarnings(); - - if( ! $success ) { - $wgOut->showFileCopyError( $tempName, $this->mSavedFile ); - return false; - } else { - wfDebug("$fname: wrote tempfile $tempName to ".$this->mSavedFile."\n"); + static function ajaxGetExistsWarning( $filename ) { + $file = wfFindFile( $filename ); + if( !$file ) { + // Force local file so we have an object to do further checks against + // if there isn't an exact match... + $file = wfLocalFile( $filename ); } - - chmod( $this->mSavedFile, 0644 ); - return true; + $s = ' '; + if ( $file ) { + $warning = self::getExistsWarning( $file ); + if ( $warning !== '' ) { + $s = "
      $warning
    "; + } + } + return $s; + } + + /** + * Render a preview of a given license for the AJAX preview on upload + * + * @param string $license + * @return string + */ + public static function ajaxGetLicensePreview( $license ) { + global $wgParser, $wgUser; + $text = '{{' . $license . '}}'; + $title = Title::makeTitle( NS_IMAGE, 'Sample.jpg' ); + $options = ParserOptions::newFromUser( $wgUser ); + + // Expand subst: first, then live templates... + $text = $wgParser->preSaveTransform( $text, $title, $wgUser, $options ); + $output = $wgParser->parse( $text, $title, $options ); + + return $output->getText(); } /** @@ -593,19 +603,14 @@ class UploadForm { */ function saveTempUploadedFile( $saveName, $tempName ) { global $wgOut; - $archive = wfImageArchiveDir( $saveName, 'temp' ); - if ( !is_dir ( $archive ) ) wfMkdirParents( $archive ); - $stash = $archive . '/' . gmdate( "YmdHis" ) . '!' . $saveName; - - $success = $this->mRemoveTempFile - ? rename( $tempName, $stash ) - : move_uploaded_file( $tempName, $stash ); - if ( !$success ) { - $wgOut->showFileCopyError( $tempName, $stash ); + $repo = RepoGroup::singleton()->getLocalRepo(); + $status = $repo->storeTemp( $saveName, $tempName ); + if ( !$status->isGood() ) { + $this->showError( $status->getWikiText() ); return false; + } else { + return $status->value; } - - return $stash; } /** @@ -618,8 +623,7 @@ class UploadForm { * @access private */ function stashSession() { - $stash = $this->saveTempUploadedFile( - $this->mUploadSaveName, $this->mUploadTempName ); + $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); if( !$stash ) { # Couldn't save the file. @@ -628,9 +632,12 @@ class UploadForm { $key = mt_rand( 0, 0x7fffffff ); $_SESSION['wsUploadData'][$key] = array( - 'mUploadTempName' => $stash, - 'mUploadSize' => $this->mUploadSize, - 'mOname' => $this->mOname ); + 'mTempPath' => $stash, + 'mFileSize' => $this->mFileSize, + 'mSrcName' => $this->mSrcName, + 'mFileProps' => $this->mFileProps, + 'version' => self::SESSION_VERSION, + ); return $key; } @@ -641,11 +648,10 @@ class UploadForm { */ function unsaveUploadedFile() { global $wgOut; - wfSuppressWarnings(); - $success = unlink( $this->mUploadTempName ); - wfRestoreWarnings(); + $repo = RepoGroup::singleton()->getLocalRepo(); + $success = $repo->freeTemp( $this->mTempPath ); if ( ! $success ) { - $wgOut->showFileDeleteError( $this->mUploadTempName ); + $wgOut->showFileDeleteError( $this->mTempPath ); return false; } else { return true; @@ -654,24 +660,6 @@ class UploadForm { /* -------------------------------------------------------------- */ - /** - * Show some text and linkage on successful upload. - * @access private - */ - function showSuccess() { - global $wgUser, $wgOut, $wgContLang; - - $sk = $wgUser->getSkin(); - $ilink = $sk->makeMediaLink( $this->mUploadSaveName, Image::imageUrl( $this->mUploadSaveName ) ); - $dname = $wgContLang->getNsText( NS_IMAGE ) . ':'.$this->mUploadSaveName; - $dlink = $sk->makeKnownLink( $dname, $dname ); - - $wgOut->addHTML( '

    ' . wfMsgHtml( 'successfulupload' ) . "

    \n" ); - $text = wfMsgWikiHtml( 'fileuploaded', $ilink, $dlink ); - $wgOut->addHTML( $text ); - $wgOut->returnToMain( false ); - } - /** * @param string $error as HTML * @access private @@ -691,7 +679,7 @@ class UploadForm { * @access private */ function uploadWarning( $warning ) { - global $wgOut; + global $wgOut, $wgContLang; global $wgUseCopyrightUpload; $this->mSessionKey = $this->stashSession(); @@ -709,12 +697,14 @@ class UploadForm { $reup = wfMsgWikiHtml( 'reuploaddesc' ); $titleObj = SpecialPage::getTitleFor( 'Upload' ); $action = $titleObj->escapeLocalURL( 'action=submit' ); + $align1 = $wgContLang->isRTL() ? 'left' : 'right'; + $align2 = $wgContLang->isRTL() ? 'right' : 'left'; if ( $wgUseCopyrightUpload ) { $copyright = " - mUploadCopyStatus ) . "\" /> - mUploadSource ) . "\" /> + mCopyrightStatus ) . "\" /> + mCopyrightSource ) . "\" /> "; } else { $copyright = ""; @@ -724,24 +714,24 @@ class UploadForm { mSessionKey ) . "\" /> - mUploadDescription ) . "\" /> + mComment ) . "\" /> mLicense ) . "\" /> - mDestFile ) . "\" /> + mDesiredDestName ) . "\" /> mWatchthis ) ) . "\" /> {$copyright}
    ' . $nsForm; - $out2 .= '' . + $out2 .= '
    ' . $nsForm; + $out2 .= '' . $sk->makeKnownLink( $wgContLang->specialPage( $this->name ), wfMsg ( 'allpages' ) ); if ( isset($dbr) && $dbr && ($n == $this->maxPerPage) && ($s = $dbr->fetchObject( $res )) ) { @@ -146,4 +146,4 @@ function showChunk( $namespace = NS_MAIN, $prefix, $including = false, $from = n } } -?> + diff --git a/includes/SpecialProtectedpages.php b/includes/SpecialProtectedpages.php index 91b138ff..122ca8fc 100644 --- a/includes/SpecialProtectedpages.php +++ b/includes/SpecialProtectedpages.php @@ -9,6 +9,10 @@ * @addtogroup SpecialPage */ class ProtectedPagesForm { + + protected $IdLevel = 'level'; + protected $IdType = 'type'; + function showList( $msg = '' ) { global $wgOut, $wgRequest; @@ -22,14 +26,15 @@ class ProtectedPagesForm { Title::purgeExpiredRestrictions(); } - $type = $wgRequest->getVal( 'type' ); - $level = $wgRequest->getVal( 'level' ); - $minsize = $wgRequest->getIntOrNull( 'minsize' ); + $type = $wgRequest->getVal( $this->IdType ); + $level = $wgRequest->getVal( $this->IdLevel ); + $sizetype = $wgRequest->getVal( 'sizetype' ); + $size = $wgRequest->getIntOrNull( 'size' ); $NS = $wgRequest->getIntOrNull( 'namespace' ); - $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $minsize ); + $pager = new ProtectedPagesPager( $this, array(), $type, $level, $NS, $sizetype, $size ); - $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $minsize ) ); + $wgOut->addHTML( $this->showOptions( $NS, $type, $level, $sizetype, $size ) ); if ( $pager->getNumRows() ) { $s = $pager->getNavigationBar(); @@ -38,7 +43,7 @@ class ProtectedPagesForm { ""; $s .= $pager->getNavigationBar(); } else { - $s = '

    ' . wfMsgHTML( 'protectedpagesempty' ) . '

    '; + $s = '

    ' . wfMsgHtml( 'protectedpagesempty' ) . '

    '; } $wgOut->addHTML( $s ); } @@ -65,6 +70,10 @@ class ProtectedPagesForm { $description_items[] = $protType; + if ( $row->pr_cascade ) { + $description_items[] = wfMsg( 'protect-summary-cascade' ); + } + $expiry_description = ''; $stxt = ''; if ( $row->pr_expiry != 'infinity' && strlen($row->pr_expiry) ) { @@ -93,7 +102,7 @@ class ProtectedPagesForm { * @param $minsize int * @private */ - function showOptions( $namespace, $type='edit', $level, $minsize ) { + function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) { global $wgScript; $action = htmlspecialchars( $wgScript ); $title = SpecialPage::getTitleFor( 'ProtectedPages' ); @@ -101,26 +110,40 @@ class ProtectedPagesForm { return "
    \n" . '
    ' . Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) . - Xml::hidden( 'title', $special ) . "\n" . - $this->getNamespaceMenu( $namespace ) . "\n" . - $this->getTypeMenu( $type ) . "\n" . + Xml::hidden( 'title', $special ) . " \n" . + $this->getNamespaceMenu( $namespace ) . " \n" . + $this->getTypeMenu( $type ) . " \n" . $this->getLevelMenu( $level ) . "
    \n" . - $this->getSizeLimit( $minsize ) . "\n" . - Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . + $this->getSizeLimit( $sizetype, $size ) . "\n" . + " " . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . "
    "; } - function getNamespaceMenu( $namespace=NULL ) { - return "" . HTMLnamespaceselector($namespace, ''); + /** + * Prepare the namespace filter drop-down; standard namespace + * selector, sans the MediaWiki namespace + * + * @param mixed $namespace Pre-select namespace + * @return string + */ + function getNamespaceMenu( $namespace = null ) { + return Xml::label( wfMsg( 'namespace' ), 'namespace' ) + . ' ' + . Xml::namespaceSelector( $namespace, '' ); } - + /** * @return string Formatted HTML * @private */ - function getSizeLimit( $minsize=0 ) { - $out = Xml::input('minsize', 9, $minsize, array( 'id' => 'minsize' ) ); - return ": " . $out; + function getSizeLimit( $sizetype, $size ) { + $out = Xml::radio( 'sizetype', 'min', ($sizetype=='min'), array('id' => 'wpmin') ); + $out .= Xml::label( wfMsg("minimum-size"), 'wpmin' ); + $out .= " ".Xml::radio( 'sizetype', 'max', ($sizetype=='max'), array('id' => 'wpmax') ); + $out .= Xml::label( wfMsg("maximum-size"), 'wpmax' ); + $out .= " ".Xml::input('size', 9, $size, array( 'id' => 'wpsize' ) ); + $out .= ' '.wfMsgHtml('pagesize'); + return $out; } /** @@ -128,28 +151,28 @@ class ProtectedPagesForm { * @private */ function getTypeMenu( $pr_type ) { - global $wgRestrictionTypes, $wgUser; + global $wgRestrictionTypes; - $out = "'; - return ": " . $out; + return + Xml::label( wfMsg('restriction-type') , $this->IdType ) . ' ' . + Xml::tags( 'select', + array( 'id' => $this->IdType, 'name' => $this->IdType ), + implode( "\n", $options ) ); } /** @@ -157,30 +180,30 @@ class ProtectedPagesForm { * @private */ function getLevelMenu( $pr_level ) { - global $wgRestrictionLevels, $wgUser; - - $out = "'; - return ": " . $out; + return + Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . ' ' . + Xml::tags( 'select', + array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ), + implode( "\n", $options ) ); } } @@ -188,16 +211,17 @@ class ProtectedPagesForm { * @todo document * @addtogroup Pager */ -class ProtectedPagesPager extends ReverseChronologicalPager { +class ProtectedPagesPager extends AlphabeticPager { public $mForm, $mConds; - function __construct( $form, $conds = array(), $type, $level, $namespace, $minsize ) { + function __construct( $form, $conds = array(), $type, $level, $namespace, $sizetype='', $size=0 ) { $this->mForm = $form; $this->mConds = $conds; $this->type = ( $type ) ? $type : 'edit'; $this->level = $level; $this->namespace = $namespace; - $this->minsize = intval($minsize); + $this->sizetype = $sizetype; + $this->size = intval($size); parent::__construct(); } @@ -208,8 +232,7 @@ class ProtectedPagesPager extends ReverseChronologicalPager { $lb = new LinkBatch; while ( $row = $this->mResult->fetchObject() ) { - $name = str_replace( ' ', '_', $row->page_title ); - $lb->add( $row->page_namespace, $name ); + $lb->add( $row->page_namespace, $row->page_title ); } $lb->execute(); @@ -218,7 +241,6 @@ class ProtectedPagesPager extends ReverseChronologicalPager { } function formatRow( $row ) { - $block = new Block; return $this->mForm->formatRow( $row ); } @@ -226,17 +248,22 @@ class ProtectedPagesPager extends ReverseChronologicalPager { $conds = $this->mConds; $conds[] = 'pr_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ); $conds[] = 'page_id=pr_page'; - $conds[] = 'page_len>=' . $this->minsize; $conds[] = 'pr_type=' . $this->mDb->addQuotes( $this->type ); - if ( $this->level ) + + if( $this->sizetype=='min' ) { + $conds[] = 'page_len>=' . $this->size; + } else if( $this->sizetype=='max' ) { + $conds[] = 'page_len<=' . $this->size; + } + + if( $this->level ) $conds[] = 'pr_level=' . $this->mDb->addQuotes( $this->level ); - if ( !is_null($this->namespace) ) + if( !is_null($this->namespace) ) $conds[] = 'page_namespace=' . $this->mDb->addQuotes( $this->namespace ); return array( 'tables' => array( 'page_restrictions', 'page' ), - 'fields' => 'max(pr_id) AS pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry', - 'conds' => $conds, - 'options' => array( 'GROUP BY' => 'page_namespace,page_title,pr_level,pr_expiry,page_len,pr_type' ), + 'fields' => 'pr_id,page_namespace,page_title,page_len,pr_type,pr_level,pr_expiry,pr_cascade', + 'conds' => $conds ); } @@ -250,11 +277,10 @@ class ProtectedPagesPager extends ReverseChronologicalPager { */ function wfSpecialProtectedpages() { - list( $limit, $offset ) = wfCheckLimits(); - $ppForm = new ProtectedPagesForm(); $ppForm->showList(); } -?> + + diff --git a/includes/SpecialRandompage.php b/includes/SpecialRandompage.php index e6c4abe8..42734274 100644 --- a/includes/SpecialRandompage.php +++ b/includes/SpecialRandompage.php @@ -105,4 +105,4 @@ class RandomPage { } } -?> + diff --git a/includes/SpecialRandomredirect.php b/includes/SpecialRandomredirect.php index 75a6b81d..b7aa3e49 100644 --- a/includes/SpecialRandomredirect.php +++ b/includes/SpecialRandomredirect.php @@ -30,4 +30,4 @@ function wfSpecialRandomredirect( $par = null ) { $wgOut->redirect( $title->getFullUrl( 'redirect=no' ) ); } -?> + diff --git a/includes/SpecialRecentchanges.php b/includes/SpecialRecentchanges.php index 84444e62..7565481b 100644 --- a/includes/SpecialRecentchanges.php +++ b/includes/SpecialRecentchanges.php @@ -269,8 +269,6 @@ function wfSpecialRecentchanges( $par, $specialPage ) { } function rcFilterByCategories ( &$rows , $categories , $any ) { - require_once ( 'Categoryfinder.php' ) ; - # Filter categories $cats = array () ; foreach ( $categories AS $cat ) { @@ -685,12 +683,12 @@ function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) { */ function rcApplyDiffStyle( $text ) { $styles = array( - 'diff' => 'background-color: white;', - 'diff-otitle' => 'background-color: white;', - 'diff-ntitle' => 'background-color: white;', - 'diff-addedline' => 'background: #cfc; font-size: smaller;', - 'diff-deletedline' => 'background: #ffa; font-size: smaller;', - 'diff-context' => 'background: #eee; font-size: smaller;', + 'diff' => 'background-color: white; color:black;', + 'diff-otitle' => 'background-color: white; color:black;', + 'diff-ntitle' => 'background-color: white; color:black;', + 'diff-addedline' => 'background: #cfc; color:black; font-size: smaller;', + 'diff-deletedline' => 'background: #ffa; color:black; font-size: smaller;', + 'diff-context' => 'background: #eee; color:black; font-size: smaller;', 'diffchange' => 'color: red; font-weight: bold; text-decoration: none;', ); @@ -702,4 +700,4 @@ function rcApplyDiffStyle( $text ) { return $text; } -?> + diff --git a/includes/SpecialRecentchangeslinked.php b/includes/SpecialRecentchangeslinked.php index 14508d3a..2a8ac32d 100644 --- a/includes/SpecialRecentchangeslinked.php +++ b/includes/SpecialRecentchangeslinked.php @@ -35,6 +35,7 @@ function wfSpecialRecentchangeslinked( $par = NULL ) { } $id = $nt->getArticleId(); + $wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $nt->getPrefixedText() ) ); $wgOut->setSubtitle( htmlspecialchars( wfMsg( 'rclsub', $nt->getPrefixedText() ) ) ); if ( ! $days ) { @@ -171,4 +172,4 @@ $GROUPBY $wgOut->addHTML( $s ); } -?> + diff --git a/includes/SpecialResetpass.php b/includes/SpecialResetpass.php index dc1e53c4..281a78b6 100644 --- a/includes/SpecialResetpass.php +++ b/includes/SpecialResetpass.php @@ -74,6 +74,8 @@ class PasswordResetForm extends SpecialPage { function showForm() { global $wgOut, $wgUser, $wgRequest; + + $wgOut->disallowUserJs(); $self = SpecialPage::getTitleFor( 'Resetpass' ); $form = @@ -160,4 +162,4 @@ class PasswordResetForm extends SpecialPage { } } -?> + diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php index 5c70d5ae..34e9dfbc 100644 --- a/includes/SpecialRevisiondelete.php +++ b/includes/SpecialRevisiondelete.php @@ -272,4 +272,4 @@ class RevisionDeleter { } } -?> + diff --git a/includes/SpecialSearch.php b/includes/SpecialSearch.php index fdaa8541..3fc8bab4 100644 --- a/includes/SpecialSearch.php +++ b/includes/SpecialSearch.php @@ -173,7 +173,8 @@ class SpecialSearch { SpecialPage::getTitleFor( 'Search' ), wfArrayToCGI( $this->powerSearchOptions(), - array( 'search' => $term ) ) ); + array( 'search' => $term ) ), + ($num < $this->limit) ); $wgOut->addHTML( "
    {$prevnext}\n" ); } @@ -184,6 +185,7 @@ class SpecialSearch { } else { $wgOut->addWikiText( '==' . wfMsg( 'notitlematches' ) . "==\n" ); } + $titleMatches->free(); } if( $textMatches ) { @@ -194,6 +196,7 @@ class SpecialSearch { # Don't show the 'no text matches' if we received title matches $wgOut->addWikiText( '==' . wfMsg( 'notextmatches' ) . "==\n" ); } + $textMatches->free(); } if ( $num == 0 ) { @@ -320,6 +323,14 @@ class SpecialSearch { $contextchars = $wgUser->getOption( 'contextchars', 50 ); $link = $sk->makeKnownLinkObj( $t ); + + //If page content is not readable, just return the title. + //This is not quite safe, but better than showing excerpts from non-readable pages + //Note that hiding the entry entirely would screw up paging. + if (!$t->userCanRead()) { + return "
  • {$link}
  • \n"; + } + $revision = Revision::newFromTitle( $t ); $text = $revision->getText(); $size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape'), @@ -403,4 +414,4 @@ class SpecialSearch { } } -?> + diff --git a/includes/SpecialShortpages.php b/includes/SpecialShortpages.php index 72b093e0..973656dd 100644 --- a/includes/SpecialShortpages.php +++ b/includes/SpecialShortpages.php @@ -89,4 +89,4 @@ function wfSpecialShortpages() { return $spp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialSpecialpages.php b/includes/SpecialSpecialpages.php index bb202358..a893966c 100644 --- a/includes/SpecialSpecialpages.php +++ b/includes/SpecialSpecialpages.php @@ -8,7 +8,9 @@ * */ function wfSpecialSpecialpages() { - global $wgOut, $wgUser; + global $wgOut, $wgUser, $wgMessageCache; + + $wgMessageCache->loadAllMessages(); $wgOut->setRobotpolicy( 'index,nofollow' ); $sk = $wgUser->getSkin(); @@ -56,4 +58,4 @@ function wfSpecialSpecialpages_gen($pages,$heading,$sk) { $wgOut->addHTML( "\n" ); } -?> + diff --git a/includes/SpecialStatistics.php b/includes/SpecialStatistics.php index 1c9e0ab6..a29811da 100644 --- a/includes/SpecialStatistics.php +++ b/includes/SpecialStatistics.php @@ -1,18 +1,19 @@ getVal( 'action' ); - $dbr = wfGetDB( DB_SLAVE ); $views = SiteStats::views(); @@ -21,18 +22,18 @@ function wfSpecialStatistics() { $images = SiteStats::images(); $total = SiteStats::pages(); $users = SiteStats::users(); + $admins = SiteStats::admins(); + $numJobs = SiteStats::jobs(); - $admins = $dbr->selectField( 'user_groups', 'COUNT(*)', array( 'ug_group' => 'sysop' ), $fname ); - $numJobs = $dbr->estimateRowCount('job'); - - if ($action == 'raw') { + if( $wgRequest->getVal( 'action' ) == 'raw' ) { $wgOut->disable(); header( 'Pragma: nocache' ); echo "total=$total;good=$good;views=$views;edits=$edits;users=$users;admins=$admins;images=$images;jobs=$numJobs\n"; return; } else { - $text = '==' . wfMsg( 'sitestats' ) . "==\n" ; - $text .= wfMsgExt( 'sitestatstext', array ( 'parsemag' ), + $text = "__NOTOC__\n"; + $text .= '==' . wfMsg( 'sitestats' ) . "==\n"; + $text .= wfMsgExt( 'sitestatstext', array( 'parsemag' ), $wgLang->formatNum( $total ), $wgLang->formatNum( $good ), $wgLang->formatNum( $views ), @@ -41,44 +42,52 @@ function wfSpecialStatistics() { $wgLang->formatNum( sprintf( '%.2f', $edits ? $views / $edits : 0 ) ), $wgLang->formatNum( $numJobs ), $wgLang->formatNum( $images ) - ); - - $text .= "\n==" . wfMsg( 'userstats' ) . "==\n"; + )."\n"; + $text .= "==" . wfMsg( 'userstats' ) . "==\n"; $text .= wfMsgExt( 'userstatstext', array ( 'parsemag' ), $wgLang->formatNum( $users ), $wgLang->formatNum( $admins ), '[[' . wfMsgForContent( 'grouppage-sysop' ) . ']]', # TODO somehow remove, kept for backwards compatibility $wgLang->formatNum( sprintf( '%.2f', $admins / $users * 100 ) ), User::makeGroupLinkWiki( 'sysop' ) - ); - - $wgOut->addWikiText( $text ); + )."\n"; global $wgDisableCounters, $wgMiserMode, $wgUser, $wgLang, $wgContLang; if( !$wgDisableCounters && !$wgMiserMode ) { - $page = $dbr->tableName( 'page' ); - $sql = "SELECT page_namespace, page_title, page_counter FROM {$page} WHERE page_is_redirect = 0 AND page_counter > 0 ORDER BY page_counter DESC"; - $sql = $dbr->limitResult($sql, 10, 0); - $res = $dbr->query( $sql, $fname ); - if( $res ) { - $wgOut->addHtml( '

    ' . wfMsgHtml( 'statistics-mostpopular' ) . '

    ' ); - $skin = $wgUser->getSkin(); - $wgOut->addHtml( '
      ' ); - while( $row = $dbr->fetchObject( $res ) ) { - $link = $skin->makeKnownLinkObj( Title::makeTitleSafe( $row->page_namespace, $row->page_title ) ); - $dirmark = $wgContLang->getDirMark(); - $wgOut->addHtml( '
    1. ' . $link . $dirmark . ' [' . $wgLang->formatNum( $row->page_counter ) . ']
    2. ' ); + $res = $dbr->select( + 'page', + array( + 'page_namespace', + 'page_title', + 'page_counter', + ), + array( + 'page_is_redirect' => 0, + 'page_counter > 0', + ), + __METHOD__, + array( + 'ORDER BY' => 'page_counter DESC', + 'LIMIT' => 10, + ) + ); + if( $res->numRows() > 0 ) { + $text .= "==" . wfMsg( 'statistics-mostpopular' ) . "==\n"; + while( $row = $res->fetchObject() ) { + $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); + if( $title instanceof Title ) + $text .= '* [[:' . $title->getPrefixedText() . ']] (' . $wgLang->formatNum( $row->page_counter ) . ")\n"; } - $wgOut->addHtml( '
    ' ); - $dbr->freeResult( $res ); + $res->free(); } } $footer = wfMsg( 'statistics-footer' ); if( !wfEmptyMsg( 'statistics-footer', $footer ) && $footer != '' ) - $wgOut->addWikiText( $footer ); - + $text .= "\n" . $footer; + + $wgOut->addWikiText( $text ); } -} -?> + +} \ No newline at end of file diff --git a/includes/SpecialUncategorizedcategories.php b/includes/SpecialUncategorizedcategories.php index e02c9bbd..67f87aa8 100644 --- a/includes/SpecialUncategorizedcategories.php +++ b/includes/SpecialUncategorizedcategories.php @@ -34,4 +34,4 @@ function wfSpecialUncategorizedcategories() { return $lpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUncategorizedimages.php b/includes/SpecialUncategorizedimages.php index 22e34669..23deefe8 100644 --- a/includes/SpecialUncategorizedimages.php +++ b/includes/SpecialUncategorizedimages.php @@ -44,4 +44,4 @@ function wfSpecialUncategorizedimages() { return $uip->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUncategorizedpages.php b/includes/SpecialUncategorizedpages.php index 408ac726..b26f6d93 100644 --- a/includes/SpecialUncategorizedpages.php +++ b/includes/SpecialUncategorizedpages.php @@ -54,4 +54,4 @@ function wfSpecialUncategorizedpages() { return $lpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialUncategorizedtemplates.php b/includes/SpecialUncategorizedtemplates.php new file mode 100644 index 00000000..fb785e00 --- /dev/null +++ b/includes/SpecialUncategorizedtemplates.php @@ -0,0 +1,31 @@ + + */ +class UncategorizedTemplatesPage extends UncategorizedPagesPage { + + var $requestedNamespace = NS_TEMPLATE; + + public function getName() { + return 'Uncategorizedtemplates'; + } + +} + +/** + * Main execution point + * + * @param mixed $par Parameter passed to the page + */ +function wfSpecialUncategorizedtemplates() { + list( $limit, $offset ) = wfCheckLimits(); + $utp = new UncategorizedTemplatesPage(); + $utp->doQuery( $offset, $limit ); +} + + diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 8e740f6d..5678a81e 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -23,6 +23,7 @@ function wfSpecialUndelete( $par ) { */ class PageArchive { protected $title; + var $fileStatus; function __construct( $title ) { if( is_null( $title ) ) { @@ -269,8 +270,9 @@ class PageArchive { $restoreFiles = $restoreAll || !empty( $fileVersions ); if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) { - $img = new Image( $this->title ); - $filesRestored = $img->restore( $fileVersions ); + $img = wfLocalFile( $this->title ); + $this->fileStatus = $img->restore( $fileVersions ); + $filesRestored = $this->fileStatus->successCount; } else { $filesRestored = 0; } @@ -280,7 +282,7 @@ class PageArchive { } else { $textRestored = 0; } - + // Touch the log! global $wgContLang; $log = new LogPage( 'delete' ); @@ -303,8 +305,12 @@ class PageArchive { if( trim( $comment ) != '' ) $reason .= ": {$comment}"; $log->addEntry( 'restore', $this->title, $reason ); - - return true; + + if ( $this->fileStatus && !$this->fileStatus->ok ) { + return false; + } else { + return true; + } } /** @@ -319,18 +325,13 @@ class PageArchive { * @return int number of revisions restored */ private function undeleteRevisions( $timestamps ) { - global $wgDBtype; - $restoreAll = empty( $timestamps ); $dbw = wfGetDB( DB_MASTER ); - $page = $dbw->tableName( 'archive' ); # Does this page already exist? We'll have to update it... $article = new Article( $this->title ); - $options = ( $wgDBtype == 'postgres' ) - ? '' // pg doesn't support this? - : 'FOR UPDATE'; + $options = 'FOR UPDATE'; $page = $dbw->selectRow( 'page', array( 'page_id', 'page_latest' ), array( 'page_namespace' => $this->title->getNamespace(), @@ -422,11 +423,10 @@ class PageArchive { } if( $revision ) { - # FIXME: Update latest if newer as well... - if( $newid ) { - // Attach the latest revision to the page... - $article->updateRevisionOn( $dbw, $revision, $previousRevId ); - + // Attach the latest revision to the page... + $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId ); + + if( $newid || $wasnew ) { // Update site stats, link tables, etc $article->createUpdates( $revision ); } @@ -453,6 +453,7 @@ class PageArchive { return $restored; } + function getFileStatus() { return $this->fileStatus; } } /** @@ -602,17 +603,24 @@ class UndeleteForm { $archive = new PageArchive( $this->mTargetObj ); $rev = $archive->getRevision( $timestamp ); - $wgOut->setPageTitle( wfMsg( 'undeletepage' ) ); - $link = $skin->makeKnownLinkObj( $self, htmlspecialchars( $this->mTargetObj->getPrefixedText() ), - 'target=' . $this->mTargetObj->getPrefixedUrl() ); - $wgOut->addHtml( '

    ' . wfMsgHtml( 'undelete-revision', $link, - htmlspecialchars( $wgLang->timeAndDate( $timestamp ) ) ) . '

    ' ); - if( !$rev ) { - $wgOut->addWikiText( wfMsg( 'undeleterevision-missing' ) ); + $wgOut->addWikiTexT( wfMsg( 'undeleterevision-missing' ) ); return; } + $wgOut->setPageTitle( wfMsg( 'undeletepage' ) ); + + $link = $skin->makeKnownLinkObj( + $self, + htmlspecialchars( $this->mTargetObj->getPrefixedText() ), + 'target=' . $this->mTargetObj->getPrefixedUrl() + ); + $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp ) ); + $user = $skin->userLink( $rev->getUser(), $rev->getUserText() ) + . $skin->userToolLinks( $rev->getUser(), $rev->getUserText() ); + + $wgOut->addHtml( '

    ' . wfMsgHtml( 'undelete-revision', $link, $time, $user ) . '

    ' ); + wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ); if( $this->mPreview ) { @@ -622,7 +630,7 @@ class UndeleteForm { $wgOut->addHtml( wfElement( 'textarea', array( - 'readonly' => true, + 'readonly' => 'readonly', 'cols' => intval( $wgUser->getOption( 'cols' ) ), 'rows' => intval( $wgUser->getOption( 'rows' ) ) ), $rev->getText() . "\n" ) . @@ -673,7 +681,7 @@ class UndeleteForm { } /* private */ function showHistory() { - global $wgLang, $wgUser, $wgOut; + global $wgLang, $wgContLang, $wgUser, $wgOut; $sk = $wgUser->getSkin(); if ( $this->mAllowed ) { @@ -699,10 +707,10 @@ class UndeleteForm { # List all stored revisions $revisions = $archive->listRevisions(); $files = $archive->listFiles(); - + $haveRevisions = $revisions && $revisions->numRows() > 0; $haveFiles = $files && $files->numRows() > 0; - + # Batch existence check on user and talk pages if( $haveRevisions ) { $batch = new LinkBatch(); @@ -727,7 +735,7 @@ class UndeleteForm { $titleObj = SpecialPage::getTitleFor( "Undelete" ); $action = $titleObj->getLocalURL( "action=submit" ); # Start the form here - $top = wfOpenElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) ); + $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) ); $wgOut->addHtml( $top ); } @@ -736,24 +744,48 @@ class UndeleteForm { $logViewer = new LogViewer( new LogReader( new FauxRequest( - array( 'page' => $this->mTargetObj->getPrefixedText(), - 'type' => 'delete' ) ) ) ); + array( + 'page' => $this->mTargetObj->getPrefixedText(), + 'type' => 'delete' + ) + ) + ), LogViewer::NO_ACTION_LINK + ); $logViewer->showList( $wgOut ); - + if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) { # Format the user-visible controls (comment field, submission button) # in a nice little table - $table = '
    '; - $table .= ''; - $table .= ''; - $table .= ''; - $table .= '
    ' . wfMsgWikiHtml( 'undeleteextrahelp' ) . '
    ' . wfMsgHtml( 'undeletecomment' ) . '' . wfInput( 'wpComment', 50, $this->mComment ) . '
     '; - $table .= wfSubmitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore' ) ); - $table .= wfElement( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ) ) ); - $table .= '
    '; + $align = $wgContLang->isRtl() ? 'left' : 'right'; + $table = + Xml::openElement( 'fieldset' ) . + Xml::openElement( 'table' ) . + "
    " . + wfMsgWikiHtml( 'undeleteextrahelp' ) . + "
    " . + Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) . + "" . + Xml::input( 'wpComment', 50, $this->mComment ) . + "
     " . + Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . + Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) . + "
    - - + - - +
    + $iw$iw
    + $reup$reup
    \n" ); @@ -755,15 +745,44 @@ class UploadForm { * @access private */ function mainUploadForm( $msg='' ) { - global $wgOut, $wgUser; - global $wgUseCopyrightUpload; + global $wgOut, $wgUser, $wgContLang; + global $wgUseCopyrightUpload, $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview; global $wgRequest, $wgAllowCopyUploads; + global $wgStylePath, $wgStyleVersion; + + $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck; + $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview; + + $adc = wfBoolToStr( $useAjaxDestCheck ); + $alp = wfBoolToStr( $useAjaxLicensePreview ); + + $wgOut->addScript( " + + " ); if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) { wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); return false; } + + if( $this->mDesiredDestName && $wgUser->isAllowed( 'deletedhistory' ) ) { + $title = Title::makeTitleSafe( NS_IMAGE, $this->mDesiredDestName ); + if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 ) { + $link = wfMsgExt( + $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted', + array( 'parse', 'replaceafter' ), + $wgUser->getSkin()->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ), + wfMsgHtml( 'restorelink', $count ) + ) + ); + $wgOut->addHtml( "
    {$link}
    " ); + } + } $cols = intval($wgUser->getOption( 'cols' )); $ew = $wgUser->getOption( 'editwidth' ); @@ -776,15 +795,15 @@ class UploadForm { "{$msg}\n" ); } $wgOut->addHTML( '
    ' ); - $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDestFile ) ); + $wgOut->addWikiText( wfMsgNoTrans( 'uploadtext', $this->mDesiredDestName ) ); $wgOut->addHTML( '
    ' ); $sourcefilename = wfMsgHtml( 'sourcefilename' ); $destfilename = wfMsgHtml( 'destfilename' ); - $summary = wfMsgWikiHtml( 'fileuploadsummary' ); + $summary = wfMsgExt( 'fileuploadsummary', 'parseinline' ); $licenses = new Licenses(); - $license = wfMsgHtml( 'license' ); + $license = wfMsgExt( 'license', array( 'parseinline' ) ); $nolicense = wfMsgHtml( 'nolicense' ); $licenseshtml = $licenses->getHtml(); @@ -794,64 +813,87 @@ class UploadForm { $titleObj = SpecialPage::getTitleFor( 'Upload' ); $action = $titleObj->escapeLocalURL(); - $encDestFile = htmlspecialchars( $this->mDestFile ); + $encDestName = htmlspecialchars( $this->mDesiredDestName ); $watchChecked = ( $wgUser->getOption( 'watchdefault' ) || - ( $wgUser->getOption( 'watchcreations' ) && $this->mDestFile == '' ) ) + ( $wgUser->getOption( 'watchcreations' ) && $this->mDesiredDestName == '' ) ) ? 'checked="checked"' : ''; + $warningChecked = $this->mIgnoreWarning ? 'checked' : ''; // Prepare form for upload or upload/copy if( $wgAllowCopyUploads && $wgUser->isAllowed( 'upload_by_url' ) ) { $filename_form = - "" . - "mDestFile?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . + "" . + "mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . wfMsgHTML( 'upload_source_file' ) . "
    " . - "" . - "mDestFile?"":"onchange='fillDestFilename(\"wpUploadFileURL\")' ") . "size='40' DISABLED />" . + "" . + "mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFileURL\")' ") . "size='40' DISABLED />" . wfMsgHtml( 'upload_source_url' ) ; } else { $filename_form = "mDestFile?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . + ($this->mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . "size='40' />" . "" ; } + if ( $useAjaxDestCheck ) { + $warningRow = " "; + $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"'; + } else { + $warningRow = ''; + $destOnkeyup = ''; + } - $wgOut->addHTML( " -
    + $encComment = htmlspecialchars( $this->mComment ); + $align1 = $wgContLang->isRTL() ? 'left' : 'right'; + $align2 = $wgContLang->isRTL() ? 'right' : 'left'; + + $wgOut->addHTML( << {$this->uploadFormTextTop} - - + - - + - - + - " ); + +EOT + ); if ( $licenseshtml != '' ) { global $wgStylePath; $wgOut->addHTML( " - - + - - "); + " ); + if( $useAjaxLicensePreview ) { + $wgOut->addHtml( " + + + + " ); + } } if ( $wgUseCopyrightUpload ) { $filestatus = wfMsgHtml ( 'filestatus' ); - $copystatus = htmlspecialchars( $this->mUploadCopyStatus ); + $copystatus = htmlspecialchars( $this->mCopyrightStatus ); $filesource = wfMsgHtml ( 'filesource' ); - $uploadsource = htmlspecialchars( $this->mUploadSource ); + $uploadsource = htmlspecialchars( $this->mCopyrightSource ); $wgOut->addHTML( " - - + + - - + + "); } - $wgOut->addHtml( " + $warningRow - + - -
    + {$filename_form}
    - + +
    - + + {$this->uploadFormTextAfterSummary}
    - +
    - +
    getSkin()->tooltipAndAccesskey( 'upload' ) . " />
    + " ); $wgOut->addWikiText( wfMsgForContent( 'edittools' ) ); $wgOut->addHTML( " @@ -906,6 +955,7 @@ class UploadForm {
    + " ); } @@ -966,12 +1016,11 @@ class UploadForm { $magic=& MimeMagic::singleton(); $mime= $magic->guessMimeType($tmpfile,false); - $fname= "SpecialUpload::verify"; - #check mime type, if desired global $wgVerifyMimeType; if ($wgVerifyMimeType) { + wfDebug ( "\n\nmime: <$mime> extension: <$extension>\n\n"); #check mime type against file extension if( !$this->verifyExtension( $mime, $extension ) ) { return new WikiErrorMsg( 'uploadcorrupt' ); @@ -998,7 +1047,7 @@ class UploadForm { return new WikiErrorMsg( 'uploadvirus', htmlspecialchars($virus) ); } - wfDebug( "$fname: all clear; passing.\n" ); + wfDebug( __METHOD__.": all clear; passing.\n" ); return true; } @@ -1010,45 +1059,46 @@ class UploadForm { * @return bool */ function verifyExtension( $mime, $extension ) { - $fname = 'SpecialUpload::verifyExtension'; - $magic =& MimeMagic::singleton(); if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) if ( ! $magic->isRecognizableExtension( $extension ) ) { - wfDebug( "$fname: passing file with unknown detected mime type; unrecognized extension '$extension', can't verify\n" ); + wfDebug( __METHOD__.": passing file with unknown detected mime type; " . + "unrecognized extension '$extension', can't verify\n" ); return true; } else { - wfDebug( "$fname: rejecting file with unknown detected mime type; recognized extension '$extension', so probably invalid file\n" ); + wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ". + "recognized extension '$extension', so probably invalid file\n" ); return false; } $match= $magic->isMatchingExtension($extension,$mime); if ($match===NULL) { - wfDebug( "$fname: no file extension known for mime type $mime, passing file\n" ); + wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" ); return true; } elseif ($match===true) { - wfDebug( "$fname: mime type $mime matches extension $extension, passing file\n" ); + wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" ); #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! return true; } else { - wfDebug( "$fname: mime type $mime mismatches file extension $extension, rejecting file\n" ); + wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); return false; } } - /** Heuristig for detecting files that *could* contain JavaScript instructions or - * things that may look like HTML to a browser and are thus - * potentially harmful. The present implementation will produce false positives in some situations. - * - * @param string $file Pathname to the temporary upload file - * @param string $mime The mime type of the file - * @param string $extension The extension of the file - * @return bool true if the file contains something looking like embedded scripts - */ + /** + * Heuristic for detecting files that *could* contain JavaScript instructions or + * things that may look like HTML to a browser and are thus + * potentially harmful. The present implementation will produce false positives in some situations. + * + * @param string $file Pathname to the temporary upload file + * @param string $mime The mime type of the file + * @param string $extension The extension of the file + * @return bool true if the file contains something looking like embedded scripts + */ function detectScript($file, $mime, $extension) { global $wgAllowTitlesInSVG; @@ -1137,93 +1187,103 @@ class UploadForm { return false; } - /** Generic wrapper function for a virus scanner program. - * This relies on the $wgAntivirus and $wgAntivirusSetup variables. - * $wgAntivirusRequired may be used to deny upload if the scan fails. - * - * @param string $file Pathname to the temporary upload file - * @return mixed false if not virus is found, NULL if the scan fails or is disabled, - * or a string containing feedback from the virus scanner if a virus was found. - * If textual feedback is missing but a virus was found, this function returns true. - */ + /** + * Generic wrapper function for a virus scanner program. + * This relies on the $wgAntivirus and $wgAntivirusSetup variables. + * $wgAntivirusRequired may be used to deny upload if the scan fails. + * + * @param string $file Pathname to the temporary upload file + * @return mixed false if not virus is found, NULL if the scan fails or is disabled, + * or a string containing feedback from the virus scanner if a virus was found. + * If textual feedback is missing but a virus was found, this function returns true. + */ function detectVirus($file) { global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; - $fname= "SpecialUpload::detectVirus"; - - if (!$wgAntivirus) { #disabled? - wfDebug("$fname: virus scanner disabled\n"); - + if ( !$wgAntivirus ) { + wfDebug( __METHOD__.": virus scanner disabled\n"); return NULL; } - if (!$wgAntivirusSetup[$wgAntivirus]) { - wfDebug("$fname: unknown virus scanner: $wgAntivirus\n"); - - $wgOut->addHTML( "
    Bad configuration: unknown virus scanner: $wgAntivirus
    \n" ); #LOCALIZE - + if ( !$wgAntivirusSetup[$wgAntivirus] ) { + wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" ); + # @TODO: localise + $wgOut->addHTML( "
    Bad configuration: unknown virus scanner: $wgAntivirus
    \n" ); return "unknown antivirus: $wgAntivirus"; } - #look up scanner configuration - $virus_scanner= $wgAntivirusSetup[$wgAntivirus]["command"]; #command pattern - $virus_scanner_codes= $wgAntivirusSetup[$wgAntivirus]["codemap"]; #exit-code map - $msg_pattern= $wgAntivirusSetup[$wgAntivirus]["messagepattern"]; #message pattern - - $scanner= $virus_scanner; #copy, so we can resolve the pattern + # look up scanner configuration + $command = $wgAntivirusSetup[$wgAntivirus]["command"]; + $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"]; + $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ? + $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null; - if (strpos($scanner,"%f")===false) $scanner.= " ".wfEscapeShellArg($file); #simple pattern: append file to scan - else $scanner= str_replace("%f",wfEscapeShellArg($file),$scanner); #complex pattern: replace "%f" with file to scan + if ( strpos( $command,"%f" ) === false ) { + # simple pattern: append file to scan + $command .= " " . wfEscapeShellArg( $file ); + } else { + # complex pattern: replace "%f" with file to scan + $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); + } - wfDebug("$fname: running virus scan: $scanner \n"); + wfDebug( __METHOD__.": running virus scan: $command \n" ); - #execute virus scanner - $code= false; + # execute virus scanner + $exitCode = false; #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. # that does not seem to be worth the pain. # Ask me (Duesentrieb) about it if it's ever needed. $output = array(); - if (wfIsWindows()) exec("$scanner",$output,$code); - else exec("$scanner 2>&1",$output,$code); - - $exit_code= $code; #remember for user feedback + if ( wfIsWindows() ) { + exec( "$command", $output, $exitCode ); + } else { + exec( "$command 2>&1", $output, $exitCode ); + } - if ($virus_scanner_codes) { #map exit code to AV_xxx constants. - if (isset($virus_scanner_codes[$code])) { - $code= $virus_scanner_codes[$code]; # explicit mapping - } else if (isset($virus_scanner_codes["*"])) { - $code= $virus_scanner_codes["*"]; # fallback mapping + # map exit code to AV_xxx constants. + $mappedCode = $exitCode; + if ( $exitCodeMap ) { + if ( isset( $exitCodeMap[$exitCode] ) ) { + $mappedCode = $exitCodeMap[$exitCode]; + } elseif ( isset( $exitCodeMap["*"] ) ) { + $mappedCode = $exitCodeMap["*"]; } } - if ($code===AV_SCAN_FAILED) { #scan failed (code was mapped to false by $virus_scanner_codes) - wfDebug("$fname: failed to scan $file (code $exit_code).\n"); + if ( $mappedCode === AV_SCAN_FAILED ) { + # scan failed (code was mapped to false by $exitCodeMap) + wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" ); - if ($wgAntivirusRequired) { return "scan failed (code $exit_code)"; } - else { return NULL; } - } - else if ($code===AV_SCAN_ABORTED) { #scan failed because filetype is unknown (probably imune) - wfDebug("$fname: unsupported file type $file (code $exit_code).\n"); + if ( $wgAntivirusRequired ) { + return "scan failed (code $exitCode)"; + } else { + return NULL; + } + } else if ( $mappedCode === AV_SCAN_ABORTED ) { + # scan failed because filetype is unknown (probably imune) + wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" ); return NULL; - } - else if ($code===AV_NO_VIRUS) { - wfDebug("$fname: file passed virus scan.\n"); - return false; #no virus found - } - else { - $output= join("\n",$output); - $output= trim($output); - - if (!$output) $output= true; #if there's no output, return true - else if ($msg_pattern) { - $groups= array(); - if (preg_match($msg_pattern,$output,$groups)) { - if ($groups[1]) $output= $groups[1]; + } else if ( $mappedCode === AV_NO_VIRUS ) { + # no virus found + wfDebug( __METHOD__.": file passed virus scan.\n" ); + return false; + } else { + $output = join( "\n", $output ); + $output = trim( $output ); + + if ( !$output ) { + $output = true; #if there's no output, return true + } elseif ( $msgPattern ) { + $groups = array(); + if ( preg_match( $msgPattern, $output, $groups ) ) { + if ( $groups[1] ) { + $output = $groups[1]; + } } } - wfDebug("$fname: FOUND VIRUS! scanner feedback: $output"); + wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" ); return $output; } } @@ -1237,7 +1297,7 @@ class UploadForm { * @access private */ function checkMacBinary() { - $macbin = new MacBinary( $this->mUploadTempName ); + $macbin = new MacBinary( $this->mTempPath ); if( $macbin->isValid() ) { $dataFile = tempnam( wfTempDir(), "WikiMacBinary" ); $dataHandle = fopen( $dataFile, 'wb' ); @@ -1245,8 +1305,8 @@ class UploadForm { wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" ); $macbin->extractData( $dataHandle ); - $this->mUploadTempName = $dataFile; - $this->mUploadSize = $macbin->dataForkLength(); + $this->mTempPath = $dataFile; + $this->mFileSize = $macbin->dataForkLength(); // We'll have to manually remove the new file if it's not kept. $this->mRemoveTempFile = true; @@ -1260,9 +1320,9 @@ class UploadForm { * @access private */ function cleanupTempFile() { - if( $this->mRemoveTempFile && file_exists( $this->mUploadTempName ) ) { - wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file $this->mUploadTempName\n" ); - unlink( $this->mUploadTempName ); + if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) { + wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" ); + unlink( $this->mTempPath ); } } @@ -1274,18 +1334,13 @@ class UploadForm { * @access private */ function checkOverwrite( $name ) { - $img = Image::newFromName( $name ); - if( is_null( $img ) ) { - // Uh... this shouldn't happen ;) - // But if it does, fall through to previous behavior - return false; - } + $img = wfFindFile( $name ); $error = ''; - if( $img->exists() ) { + if( $img ) { global $wgUser, $wgOut; if( $img->isLocal() ) { - if( !$wgUser->isAllowed( 'reupload' ) ) { + if( !self::userCanReUpload( $wgUser, $img->name ) ) { $error = 'fileexists-forbidden'; } } else { @@ -1305,5 +1360,64 @@ class UploadForm { return true; } + /** + * Check if a user is the last uploader + * + * @param User $user + * @param string $img, image name + * @return bool + */ + public static function userCanReUpload( User $user, $img ) { + if( $user->isAllowed( 'reupload' ) ) + return true; // non-conditional + if( !$user->isAllowed( 'reupload-own' ) ) + return false; + + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow('image', + /* SELECT */ 'img_user', + /* WHERE */ array( 'img_name' => $img ) + ); + if ( !$row ) + return false; + + return $user->getID() == $row->img_user; + } + + /** + * Display an error with a wikitext description + */ + function showError( $description ) { + global $wgOut; + $wgOut->setPageTitle( wfMsg( "internalerror" ) ); + $wgOut->setRobotpolicy( "noindex,nofollow" ); + $wgOut->setArticleRelated( false ); + $wgOut->enableClientCache( false ); + $wgOut->addWikiText( $description ); + } + + /** + * Get the initial image page text based on a comment and optional file status information + */ + static function getInitialPageText( $comment, $license, $copyStatus, $source ) { + global $wgUseCopyrightUpload; + if ( $wgUseCopyrightUpload ) { + if ( $license != '' ) { + $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } + $pageText = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n" . + '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" . + "$licensetxt" . + '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ; + } else { + if ( $license != '' ) { + $filedesc = $comment == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $comment . "\n"; + $pageText = $filedesc . + '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } else { + $pageText = $comment; + } + } + return $pageText; + } } -?> diff --git a/includes/SpecialUploadMogile.php b/includes/SpecialUploadMogile.php index 27af62e7..438e1df4 100644 --- a/includes/SpecialUploadMogile.php +++ b/includes/SpecialUploadMogile.php @@ -133,4 +133,4 @@ class UploadFormMogile extends UploadForm { } } } -?> + diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index e8f33b8d..f358c1fd 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -8,7 +8,6 @@ * constructor */ function wfSpecialUserlogin() { - global $wgCommandLineMode; global $wgRequest; if( session_id() == '' ) { wfSetupSession(); @@ -32,6 +31,7 @@ class LoginForm { const WRONG_PASS = 5; const EMPTY_PASS = 6; const RESET_PASS = 7; + const ABORTED = 8; var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword; @@ -264,6 +264,11 @@ class LoginForm { $this->mainLoginForm( wfMsg( 'passwordtooshort', $wgMinimalPasswordLength ) ); return false; } + + # Set some additional data so the AbortNewAccount hook can be + # used for more than just username validation + $u->setEmail( $this->mEmail ); + $u->setRealName( $this->mRealName ); $abortError = ''; if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) { @@ -290,7 +295,7 @@ class LoginForm { return false; } - return $this->initUser( $u ); + return $this->initUser( $u, false ); } /** @@ -298,10 +303,11 @@ class LoginForm { * Give it a User object that has been initialised with a name. * * @param $u User object. + * @param $autocreate boolean -- true if this is an autocreation via auth plugin * @return User object. * @private */ - function initUser( $u ) { + function initUser( $u, $autocreate ) { global $wgAuth; $u->addToDatabase(); @@ -314,7 +320,7 @@ class LoginForm { $u->setRealName( $this->mRealName ); $u->setToken(); - $wgAuth->initUser( $u ); + $wgAuth->initUser( $u, $autocreate ); $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); $u->saveSettings(); @@ -353,7 +359,7 @@ class LoginForm { */ if ( $wgAuth->autoCreate() && $wgAuth->userExists( $u->getName() ) ) { if ( $wgAuth->authenticate( $u->getName(), $this->mPassword ) ) { - $u = $this->initUser( $u ); + $u = $this->initUser( $u, true ); } else { return self::WRONG_PLUGIN_PASS; } @@ -364,8 +370,13 @@ class LoginForm { $u->load(); } - if (!$u->checkPassword( $this->mPassword )) { + // Give general extensions, such as a captcha, a chance to abort logins + $abort = self::ABORTED; + if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort ) ) ) { + return $abort; + } + if (!$u->checkPassword( $this->mPassword )) { if( $u->checkTemporaryPassword( $this->mPassword ) ) { // The e-mailed temporary password should not be used // for actual logins; that's a very sloppy habit, @@ -394,16 +405,18 @@ class LoginForm { // reset form; bot interfaces etc will probably just // fail cleanly here. // - return self::RESET_PASS; + $retval = self::RESET_PASS; } else { - return '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; + $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS; } } else { $wgAuth->updateUser( $u ); $wgUser = $u; - return self::SUCCESS; + $retval = self::SUCCESS; } + wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) ); + return $retval; } function processLogin() { @@ -697,6 +710,7 @@ class LoginForm { $wgOut->setPageTitle( wfMsg( 'userlogin' ) ); $wgOut->setRobotpolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); + $wgOut->disallowUserJs(); // just in case... $wgOut->addTemplate( $template ); } @@ -809,4 +823,4 @@ class LoginForm { return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) ); } } -?> + diff --git a/includes/SpecialUserlogout.php b/includes/SpecialUserlogout.php index 9f1bdb3a..6e464ced 100644 --- a/includes/SpecialUserlogout.php +++ b/includes/SpecialUserlogout.php @@ -23,4 +23,4 @@ function wfSpecialUserlogout() { } } -?> + diff --git a/includes/SpecialUserrights.php b/includes/SpecialUserrights.php index d12a9cc4..b97e5168 100644 --- a/includes/SpecialUserrights.php +++ b/includes/SpecialUserrights.php @@ -27,7 +27,7 @@ class UserrightsForm extends HTMLForm { var $action; /** Constructor*/ - function UserrightsForm ( &$request ) { + public function __construct( &$request ) { $this->mPosted = $request->wasPosted(); $this->mRequest =& $request; $this->mName = 'userrights'; @@ -74,7 +74,7 @@ class UserrightsForm extends HTMLForm { * @param string $reason Reason for group change * */ - function saveUserGroups( $username, $removegroup, $addgroup, $reason ) { + function saveUserGroups( $username, $removegroup, $addgroup, $reason = '' ) { global $wgOut; $u = User::newFromName($username); @@ -94,13 +94,17 @@ class UserrightsForm extends HTMLForm { if(isset($removegroup)) { $newGroups = array_diff($newGroups, $removegroup); foreach( $removegroup as $group ) { - $u->removeGroup( $group ); + if ( $this->canRemove( $group ) ) { + $u->removeGroup( $group ); + } } } if(isset($addgroup)) { $newGroups = array_merge($newGroups, $addgroup); foreach( $addgroup as $group ) { - $u->addGroup( $group ); + if ( $this->canAdd( $group ) ) { + $u->addGroup( $group ); + } } } $newGroups = array_unique( $newGroups ); @@ -139,7 +143,7 @@ class UserrightsForm extends HTMLForm { */ function editUserGroupsForm($username) { global $wgOut; - + $user = User::newFromName($username); if( is_null( $user ) ) { $wgOut->addWikiText( wfMsg( 'nouserspecified' ) ); @@ -149,12 +153,38 @@ class UserrightsForm extends HTMLForm { return; } - $groups = $user->getGroups(); - $this->showEditUserGroupsForm( $username, $groups ); + $this->showEditUserGroupsForm( $username, $user->getGroups() ); + $this->showLogFragment( $user, $wgOut ); } - function showEditUserGroupsForm( $username, $groups ) { + /** + * Go through used and available groups and return the ones that this + * form will be able to manipulate based on the current user's system + * permissions. + * + * @param $groups Array: list of groups the given user is in + * @return Array: Tuple of addable, then removable groups + */ + protected function splitGroups( $groups ) { + list($addable, $removable) = array_values( $this->changeableGroups() ); + $removable = array_intersect($removable, $groups ); // Can't remove groups the user doesn't have + $addable = array_diff( $addable, $groups ); // Can't add groups the user does have + + return array( $addable, $removable ); + } + + /** + * Show the form to edit group memberships. + * + * @todo make all CSS-y and semantic + * @param $username String: Name of user you're editing + * @param $groups Array: Array of groups the user is in + */ + protected function showEditUserGroupsForm( $username, $groups ) { global $wgOut, $wgUser; + + list( $addable, $removable ) = $this->splitGroups( $groups ); + $wgOut->addHTML( Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) . Xml::hidden( 'user-editname', $username ) . @@ -162,14 +192,15 @@ class UserrightsForm extends HTMLForm { Xml::openElement( 'fieldset' ) . Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) . $wgOut->parse( wfMsg( 'editinguser', $username ) ) . + $this->explainRights() . " @@ -197,5 +228,175 @@ class UserrightsForm extends HTMLForm { Xml::closeElement( 'form' ) . "\n" ); } -} // end class UserrightsForm -?> + + /** + * Prepare a list of groups the user is able to add and remove + * + * @return string + */ + private function explainRights() { + global $wgUser, $wgLang; + + $out = array(); + list( $add, $remove ) = array_values( $this->changeableGroups() ); + + if( count( $add ) > 0 ) + $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) ); + if( count( $remove ) > 0 ) + $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) ); + + return count( $out ) > 0 + ? implode( ' ', $out ) + : wfMsgExt( 'userrights-available-none', 'parseinline' ); + } + + /** + * Adds the element + */ + private function removeSelect( $groups ) { + return $this->doSelect( $groups, 'member' ); + } + + /** + * Adds the element + */ + private function addSelect( $groups ) { + return $this->doSelect( $groups, 'available' ); + } + + /** + * Adds the element + */ + private function doSelect( $groups, $name ) { + $ret = wfMsgHtml( "{$this->mName}-groups$name" ) . + Xml::openElement( 'select', array( + 'name' => "{$name}[]", + 'multiple' => 'multiple', + 'size' => '6', + 'style' => 'width: 100%;' + ) + ); + foreach ($groups as $group) { + $ret .= Xml::element( 'option', array( 'value' => $group ), User::getGroupName( $group ) ); + } + $ret .= Xml::closeElement( 'select' ); + return $ret; + } + + /** + * @param string $group The name of the group to check + * @return bool Can we remove the group? + */ + private function canRemove( $group ) { + // $this->changeableGroups()['remove'] doesn't work, of course. Thanks, + // PHP. + $groups = $this->changeableGroups(); + return in_array( $group, $groups['remove'] ); + } + + /** + * @param string $group The name of the group to check + * @return bool Can we add the group? + */ + private function canAdd( $group ) { + $groups = $this->changeableGroups(); + return in_array( $group, $groups['add'] ); + } + + /** + * Returns an array of the groups that the user can add/remove. + * + * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) ) + */ + private function changeableGroups() { + global $wgUser; + + $groups = array( 'add' => array(), 'remove' => array() ); + $addergroups = $wgUser->getEffectiveGroups(); + + foreach ($addergroups as $addergroup) { + $groups = array_merge_recursive( + $groups, $this->changeableByGroup($addergroup) + ); + $groups['add'] = array_unique( $groups['add'] ); + $groups['remove'] = array_unique( $groups['remove'] ); + } + return $groups; + } + + /** + * Returns an array of the groups that a particular group can add/remove. + * + * @param String $group The group to check for whether it can add/remove + * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) ) + */ + private function changeableByGroup( $group ) { + global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups; + + if( empty($wgGroupPermissions[$group]['userrights']) ) { + // This group doesn't give the right to modify anything + return array( 'add' => array(), 'remove' => array() ); + } + if( empty($wgAddGroups[$group]) and empty($wgRemoveGroups[$group]) ) { + // This group gives the right to modify everything (reverse- + // compatibility with old "userrights lets you change + // everything") + return array( + 'add' => User::getAllGroups(), + 'remove' => User::getAllGroups() + ); + } + + // Okay, it's not so simple, we have to go through the arrays + $groups = array( 'add' => array(), 'remove' => array() ); + if( empty($wgAddGroups[$group]) ) { + // Don't add anything to $groups + } elseif( $wgAddGroups[$group] === true ) { + // You get everything + $groups['add'] = User::getAllGroups(); + } elseif( is_array($wgAddGroups[$group]) ) { + $groups['add'] = $wgAddGroups[$group]; + } + + // Same thing for remove + if( empty($wgRemoveGroups[$group]) ) { + } elseif($wgRemoveGroups[$group] === true ) { + $groups['remove'] = User::getAllGroups(); + } elseif( is_array($wgRemoveGroups[$group]) ) { + $groups['remove'] = $wgRemoveGroups[$group]; + } + return $groups; + } + + /** + * Show a rights log fragment for the specified user + * + * @param User $user User to show log for + * @param OutputPage $output OutputPage to use + */ + protected function showLogFragment( $user, $output ) { + $viewer = new LogViewer( + new LogReader( + new FauxRequest( + array( + 'type' => 'rights', + 'page' => $user->getUserPage()->getPrefixedUrl(), + ) + ) + ) + ); + $output->addHtml( "

    " . htmlspecialchars( LogPage::logName( 'rights' ) ) . "

    \n" ); + $viewer->showList( $output ); + } + +} \ No newline at end of file diff --git a/includes/SpecialVersion.php b/includes/SpecialVersion.php index 6de2da11..a6a132e0 100644 --- a/includes/SpecialVersion.php +++ b/includes/SpecialVersion.php @@ -148,10 +148,14 @@ class SpecialVersion { /** Callback to sort extensions by type */ function compare( $a, $b ) { - if ( $a['name'] === $b['name'] ) + global $wgLang; + if( $a['name'] === $b['name'] ) { return 0; - else - return Language::lc( $a['name'] ) > Language::lc( $b['name'] ) ? 1 : -1; + } else { + return $wgLang->lc( $a['name'] ) > $wgLang->lc( $b['name'] ) + ? 1 + : -1; + } } function formatCredits( $name, $version = null, $author = null, $url = null, $description = null) { @@ -250,7 +254,10 @@ class SpecialVersion { * @return mixed */ function arrayToString( $list ) { - if ( ! is_array( $list ) ) { + if( is_object( $list ) ) { + $class = get_class( $list ); + return "($class)"; + } elseif ( ! is_array( $list ) ) { return $list; } else { $class = get_class( $list[0] ); @@ -308,4 +315,4 @@ class SpecialVersion { } /**#@-*/ -?> + diff --git a/includes/SpecialWantedcategories.php b/includes/SpecialWantedcategories.php index 27a9f176..f3e8966c 100644 --- a/includes/SpecialWantedcategories.php +++ b/includes/SpecialWantedcategories.php @@ -76,4 +76,4 @@ function wfSpecialWantedCategories() { $wpp->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SpecialWantedpages.php b/includes/SpecialWantedpages.php index 8b700209..5fc45a88 100644 --- a/includes/SpecialWantedpages.php +++ b/includes/SpecialWantedpages.php @@ -63,46 +63,48 @@ class WantedPagesPage extends QueryPage { $db->dataSeek( $res, 0 ); } - - function formatResult( $skin, $result ) { - global $wgLang; - + /** + * Format an individual result + * + * @param Skin $skin Skin to use for UI elements + * @param object $result Result row + * @return string + */ + public function formatResult( $skin, $result ) { $title = Title::makeTitleSafe( $result->namespace, $result->title ); - - if( $this->isCached() ) { - # Check existence; which is stored in the link cache - if( !$title->exists() ) { - # Make a redlink - $pageLink = $skin->makeBrokenLinkObj( $title ); + if( $title instanceof Title ) { + if( $this->isCached() ) { + $pageLink = $title->exists() + ? '' . $skin->makeLinkObj( $title ) . '' + : $skin->makeBrokenLinkObj( $title ); } else { - # Make a a struck-out normal link - $pageLink = "" . $skin->makeLinkObj( $title ) . ""; - } + $pageLink = $skin->makeBrokenLinkObj( $title ); + } + return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) ); } else { - # Not cached? Don't bother checking existence; it can't - $pageLink = $skin->makeBrokenLinkObj( $title ); + $tsafe = htmlspecialchars( $result->title ); + return "Invalid title in result set; {$tsafe}"; } - - # Make a link to "what links here" if it's required - $wlhLink = $this->nlinks - ? $this->makeWlhLink( $title, $skin, - wfMsgExt( 'nlinks', array( 'parsemag', 'escape'), - $wgLang->formatNum( $result->value ) ) ) - : null; - - return wfSpecialList($pageLink, $wlhLink); } /** - * Make a "what links here" link for a specified title - * @param $title Title to make the link for - * @param $skin Skin to use - * @param $text Link text + * Make a "what links here" link for a specified result if required + * + * @param Title $title Title to make the link for + * @param Skin $skin Skin to use + * @param object $result Result row * @return string */ - function makeWlhLink( &$title, &$skin, $text ) { - $wlhTitle = SpecialPage::getTitleFor( 'Whatlinkshere' ); - return $skin->makeKnownLinkObj( $wlhTitle, $text, 'target=' . $title->getPrefixedUrl() ); + private function makeWlhLink( $title, $skin, $result ) { + global $wgLang; + if( $this->nlinks ) { + $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' ); + $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $result->value ) ); + return $skin->makeKnownLinkObj( $wlh, $label, 'target=' . $title->getPrefixedUrl() ); + } else { + return null; + } } } @@ -128,4 +130,4 @@ function wfSpecialWantedpages( $par = null, $specialPage ) { $wpp->doQuery( $offset, $limit, !$inc ); } -?> + diff --git a/includes/SpecialWatchlist.php b/includes/SpecialWatchlist.php index 2e660bd5..e9aa7e68 100644 --- a/includes/SpecialWatchlist.php +++ b/includes/SpecialWatchlist.php @@ -15,7 +15,7 @@ require_once( dirname(__FILE__) . '/SpecialRecentchanges.php' ); * @param $par Parameter passed to the page */ function wfSpecialWatchlist( $par ) { - global $wgUser, $wgOut, $wgLang, $wgRequest, $wgContLang; + global $wgUser, $wgOut, $wgLang, $wgRequest; global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker; global $wgEnotifWatchlist; $fname = 'wfSpecialWatchlist'; @@ -30,12 +30,24 @@ function wfSpecialWatchlist( $par ) { $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ), wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() ); $wgOut->addHtml( wfMsgWikiHtml( 'watchlistanontext', $llink ) ); return; - } else { - $wgOut->setPageTitle( wfMsg( 'watchlist' ) ); - $wgOut->setSubtitle( wfMsgWikiHtml( 'watchlistfor', htmlspecialchars( $wgUser->getName() ) ) ); } + + $wgOut->setPageTitle( wfMsg( 'watchlist' ) ); + + $sub = wfMsgExt( 'watchlistfor', 'parseinline', $wgUser->getName() ); + $sub .= '
    ' . WatchlistEditor::buildTools( $wgUser->getSkin() ); + $wgOut->setSubtitle( $sub ); - if( wlHandleClear( $wgOut, $wgRequest, $par ) ) { + if( ( $mode = WatchlistEditor::getMode( $wgRequest, $par ) ) !== false ) { + $editor = new WatchlistEditor(); + $editor->execute( $wgUser, $wgOut, $wgRequest, $mode ); + return; + } + + $uid = $wgUser->getId(); + if( $wgEnotifWatchlist && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) { + $wgUser->clearAllNotifications( $uid ); + $wgOut->redirect( $specialTitle->getFullUrl() ); return; } @@ -72,37 +84,6 @@ function wfSpecialWatchlist( $par ) { $nameSpaceClause = ''; } - # Watchlist editing - $action = $wgRequest->getVal( 'action' ); - $remove = $wgRequest->getVal( 'remove' ); - $id = $wgRequest->getArray( 'id' ); - - $uid = $wgUser->getID(); - if( $wgEnotifWatchlist && $wgRequest->getVal( 'reset' ) && $wgRequest->wasPosted() ) { - $wgUser->clearAllNotifications( $uid ); - } - - # Deleting items from watchlist - if(($action == 'submit') && isset($remove) && is_array($id)) { - $wgOut->addWikiText( wfMsg( 'removingchecked' ) ); - $wgOut->addHTML( "
      \n" ); - foreach($id as $one) { - $t = Title::newFromURL( $one ); - if( !is_null( $t ) ) { - $wl = WatchedItem::fromUserTitle( $wgUser, $t ); - if( $wl->removeWatch() === false ) { - $wgOut->addHTML( '
    • ' . wfMsg( 'couldntremove', htmlspecialchars($one) ) . "
    • \n" ); - } else { - wfRunHooks('UnwatchArticle', array(&$wgUser, new Article($t))); - $wgOut->addHTML( '
    • [[' . htmlspecialchars($one) . "]]
    • \n" ); - } - } else { - $wgOut->addHTML( '
    • ' . wfMsg( 'iteminvalidname', htmlspecialchars($one) ) . "
    • \n" ); - } - } - $wgOut->addHTML( "
    \n

    " . wfMsg( 'wldone' ) . "

    \n" ); - } - $dbr = wfGetDB( DB_SLAVE, 'watchlist' ); list( $page, $watchlist, $recentchanges ) = $dbr->tableNamesN( 'page', 'watchlist', 'recentchanges' ); @@ -143,7 +124,6 @@ function wfSpecialWatchlist( $par ) { if ( $days <= 0 ) { $andcutoff = ''; - $npages = wfMsg( 'watchlistall1' ); } else { $andcutoff = "AND rc_timestamp > '".$dbr->timestamp( time() - intval( $days * 86400 ) )."'"; /* @@ -152,82 +132,6 @@ function wfSpecialWatchlist( $par ) { $s = $dbr->fetchObject( $res ); $npages = $s->n; */ - $npages = 40000 * $days; - } - - /* Edit watchlist form */ - if($wgRequest->getBool('edit') || $par == 'edit' ) { - $wgOut->addWikiText( wfMsgExt( 'watchlistcontains', array( 'parseinline' ), $wgLang->formatNum( $nitems ) ) . - "\n\n" . wfMsg( 'watcheditlist' ) ); - - $wgOut->addHTML( 'escapeLocalUrl( 'action=submit' ) . - "' method='post'>\n" ); - -# Patch A2 -# The following was proposed by KTurner 07.11.2004 to T.Gries -# $sql = "SELECT distinct (wl_namespace & ~1),wl_title FROM $watchlist WHERE wl_user=$uid"; - $sql = "SELECT wl_namespace, wl_title, page_is_redirect FROM $watchlist LEFT JOIN $page ON wl_namespace = page_namespace AND wl_title = page_title WHERE wl_user=$uid"; - - $res = $dbr->query( $sql, $fname ); - - # Batch existence check - $linkBatch = new LinkBatch(); - while( $row = $dbr->fetchObject( $res ) ) - $linkBatch->addObj( Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ) ); - $linkBatch->execute(); - - if( $dbr->numRows( $res ) > 0 ) - $dbr->dataSeek( $res, 0 ); # Let's do the time warp again! - - $sk = $wgUser->getSkin(); - - $list = array(); - while( $s = $dbr->fetchObject( $res ) ) { - $list[$s->wl_namespace][$s->wl_title] = $s->page_is_redirect; - } - - // TODO: Display a TOC - foreach($list as $ns => $titles) { - if (Namespace::isTalk($ns)) - continue; - if ($ns != NS_MAIN) - $wgOut->addHTML( '

    ' . $wgContLang->getFormattedNsText( $ns ) . '

    ' ); - $wgOut->addHTML( '
      ' ); - foreach( $titles as $title => $redir ) { - $titleObj = Title::makeTitle( $ns, $title ); - if( is_null( $titleObj ) ) { - $wgOut->addHTML( - '\n" - ); - } else { - global $wgContLang; - $toolLinks = array(); - $pageLink = $sk->makeLinkObj( $titleObj ); - $toolLinks[] = $sk->makeLinkObj( $titleObj->getTalkPage(), $wgLang->getNsText( NS_TALK ) ); - if( $titleObj->exists() ) - $toolLinks[] = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml( 'history_short' ), 'action=history' ); - $toolLinks = '(' . implode( ' | ', $toolLinks ) . ')'; - $checkbox = ' ' . ( $wgContLang->isRTL() ? '‏' : '‎' ); - if( $redir ) { - $spanopen = ''; - $spanclosed = ''; - } else { - $spanopen = $spanclosed = ''; - } - - $wgOut->addHTML( "
    • {$checkbox}{$spanopen}{$pageLink}{$spanclosed} {$toolLinks}
    • \n" ); - } - } - $wgOut->addHTML( '
    ' ); - } - $wgOut->addHTML( - wfSubmitButton( wfMsg('removechecked'), array('name' => 'remove') ) . - "\n\n" - ); - - return; } # If the watchlist is relatively short, it's simplest to zip @@ -257,17 +161,17 @@ function wfSpecialWatchlist( $par ) { $andLatest=''; $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) ); } else { - # Top log Ids for a page are not stored - $andLatest= 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') '; + $andLatest= 'AND rc_this_oldid=page_latest'; $limitWatchlist = ''; } - # TODO: Consider removing the third parameter - $header .= wfMsgExt( 'watchdetails', array( 'parsemag' ), $wgLang->formatNum( $nitems ), - $wgLang->formatNum( $npages ), '', - $specialTitle->getFullUrl( 'edit=yes' ) ); + $header .= wfMsgExt( 'watchlist-details', array( 'parsemag' ), $wgLang->formatNum( $nitems ) ); $wgOut->addWikiText( $header ); + # Show a message about slave lag, if applicable + if( ( $lag = $dbr->getLag() ) > 0 ) + $wgOut->showLagWarning( $lag ); + if ( $wgEnotifWatchlist && $wgShowUpdatedMarker ) { $wgOut->addHTML( '\n"); @@ -105,25 +106,29 @@ class WhatLinksHerePage { } if ( $from ) { - $offsetCond = "page_id >= $from"; - } else { - $offsetCond = false; - } - $options['ORDER BY'] = 'page_id'; + $from = (int)$from; // just in case + $tlConds[] = "tl_from >= $from"; + $plConds[] = "pl_from >= $from"; + } // Read an extra row as an at-end check $queryLimit = $limit + 1; + + // enforce join order, sometimes namespace selector may + // trigger filesorts which are far less efficient than scanning many entries + $options[] = 'STRAIGHT_JOIN'; + $options['LIMIT'] = $queryLimit; - if ( $offsetCond ) { - $tlConds[] = $offsetCond; - $plConds[] = $offsetCond; - } $fields = array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ); + $options['ORDER BY'] = 'pl_from'; $plRes = $dbr->select( array( 'pagelinks', 'page' ), $fields, $plConds, $fname, $options ); + + $options['ORDER BY'] = 'tl_from'; $tlRes = $dbr->select( array( 'templatelinks', 'page' ), $fields, $tlConds, $fname, $options ); + if ( !$dbr->numRows( $plRes ) && !$dbr->numRows( $tlRes ) ) { if ( 0 == $level && !isset( $this->namespace ) ) { // really no links to here @@ -226,6 +231,14 @@ class WhatLinksHerePage { $wgOut->addHTML( ' (' . implode( ', ', $props ) . ') ' ); } + # Space for utilities links, with a what-links-here link provided + $wlh = $this->skin->makeKnownLinkObj( + SpecialPage::getTitleFor( 'Whatlinkshere' ), + wfMsgHtml( 'whatlinkshere-links' ), + 'target=' . $nt->getPrefixedUrl() + ); + $wgOut->addHtml( ' (' . $wlh . ')' ); + if ( $row->page_is_redirect ) { if ( $level < 2 ) { $this->showIndirectLinks( $level + 1, $nt, 500 ); @@ -312,4 +325,4 @@ class WhatLinksHerePage { } -?> + diff --git a/includes/SpecialWithoutinterwiki.php b/includes/SpecialWithoutinterwiki.php index e5341d5d..33464586 100644 --- a/includes/SpecialWithoutinterwiki.php +++ b/includes/SpecialWithoutinterwiki.php @@ -12,9 +12,9 @@ class WithoutInterwikiPage extends PageQueryPage { function getName() { return 'Withoutinterwiki'; } - + function getPageHeader() { - return '

    ' . wfMsgHtml( 'withoutinterwiki-header' ) . '

    '; + return '

    ' . wfMsgExt( 'withoutinterwiki-header', array( 'parseinline' ) ) . '

    '; } function sortDescending() { @@ -24,7 +24,7 @@ class WithoutInterwikiPage extends PageQueryPage { function isExpensive() { return true; } - + function isSyndicated() { return false; } @@ -44,7 +44,7 @@ class WithoutInterwikiPage extends PageQueryPage { AND page_namespace=" . NS_MAIN . " AND page_is_redirect = 0"; } - + } function wfSpecialWithoutinterwiki() { @@ -53,4 +53,4 @@ function wfSpecialWithoutinterwiki() { $wip->doQuery( $offset, $limit ); } -?> + diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php index 700fc8ef..5d7350a9 100644 --- a/includes/SquidUpdate.php +++ b/includes/SquidUpdate.php @@ -22,7 +22,7 @@ class SquidUpdate { $this->urlArr = $urlArr; } - /* static */ function newFromLinksTo( &$title ) { + static function newFromLinksTo( &$title ) { $fname = 'SquidUpdate::newFromLinksTo'; wfProfileIn( $fname ); @@ -49,7 +49,7 @@ class SquidUpdate { return new SquidUpdate( $blurlArr ); } - /* static */ function newFromTitles( &$titles, $urlArr = array() ) { + static function newFromTitles( &$titles, $urlArr = array() ) { global $wgMaxSquidPurgeTitles; if ( count( $titles ) > $wgMaxSquidPurgeTitles ) { $titles = array_slice( $titles, 0, $wgMaxSquidPurgeTitles ); @@ -60,7 +60,7 @@ class SquidUpdate { return new SquidUpdate( $urlArr ); } - /* static */ function newSimplePurge( &$title ) { + static function newSimplePurge( &$title ) { $urlArr = $title->getSquidURLs(); return new SquidUpdate( $urlArr ); } @@ -74,16 +74,21 @@ class SquidUpdate { (example: $urlArr[] = 'http://my.host/something') XXX report broken Squids per mail or log */ - /* static */ function purge( $urlArr ) { + static function purge( $urlArr ) { global $wgSquidServers, $wgHTCPMulticastAddress, $wgHTCPPort; /*if ( (@$wgSquidServers[0]) == 'echo' ) { echo implode("
    \n", $urlArr) . "
    \n"; return; }*/ + + if( empty( $urlArr ) ) { + return; + } - if ( $wgHTCPMulticastAddress && $wgHTCPPort ) - SquidUpdate::HTCPPurge( $urlArr ); + if ( $wgHTCPMulticastAddress && $wgHTCPPort ) { + return SquidUpdate::HTCPPurge( $urlArr ); + } $fname = 'SquidUpdate::purge'; wfProfileIn( $fname ); @@ -189,7 +194,7 @@ class SquidUpdate { wfProfileOut( $fname ); } - /* static */ function HTCPPurge( $urlArr ) { + static function HTCPPurge( $urlArr ) { global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort; $fname = 'SquidUpdate::HTCPPurge'; wfProfileIn( $fname ); @@ -277,4 +282,4 @@ class SquidUpdate { return $url; } } -?> + diff --git a/includes/StreamFile.php b/includes/StreamFile.php index dc653e57..8ecaa4f0 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -31,6 +31,9 @@ function wfStreamFile( $fname ) { header('Content-type: application/x-wiki'); } + global $wgContLanguageCode; + header( "Content-Disposition: inline;filename*=utf-8'$wgContLanguageCode'" . urlencode( basename( $fname ) ) ); + if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); $sinceTime = strtotime( $modsince ); @@ -69,4 +72,4 @@ function wfGetType( $filename ) { } } -?> + diff --git a/includes/StringUtils.php b/includes/StringUtils.php index 9a451aa8..374fb002 100644 --- a/includes/StringUtils.php +++ b/includes/StringUtils.php @@ -300,4 +300,4 @@ class ReplacementArray { } } -?> + diff --git a/includes/StubObject.php b/includes/StubObject.php index 894550cd..a9a6bde9 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -134,4 +134,4 @@ class StubUser extends StubObject { } } -?> + diff --git a/includes/Title.php b/includes/Title.php index 0ff2e807..c4db4172 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -40,13 +40,14 @@ class Title { * Please use the accessor functions */ - /**#@+ + /**#@+ * @private */ var $mTextform; # Text form (spaces not underscores) of the main part var $mUrlform; # URL-encoded form of the main part var $mDbkeyform; # Main part with underscores + var $mUserCaseDBKey; # DB key with the initial letter in the case specified by the user var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants var $mInterwiki; # Interwiki prefix (or null string) var $mFragment; # Title fragment (i.e. the bit after the #) @@ -232,7 +233,7 @@ class Title { $t = new Title(); $t->mInterwiki = ''; $t->mFragment = ''; - $t->mNamespace = intval( $ns ); + $t->mNamespace = $ns = intval( $ns ); $t->mDbkeyform = str_replace( ' ', '_', $title ); $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); @@ -268,32 +269,33 @@ class Title { } /** - * Create a new Title for a redirect - * @param string $text the redirect title text - * @return Title the new object, or NULL if the text is not a - * valid redirect + * Extract a redirect destination from a string and return the + * Title, or null if the text doesn't contain a valid redirect + * + * @param string $text Text with possible redirect + * @return Title */ public static function newFromRedirect( $text ) { - $mwRedir = MagicWord::get( 'redirect' ); - $rt = NULL; - if ( $mwRedir->matchStart( $text ) ) { + $redir = MagicWord::get( 'redirect' ); + if( $redir->matchStart( $text ) ) { + // Extract the first link and see if it's usable $m = array(); - if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) { - # categories are escaped using : for example one can enter: - # #REDIRECT [[:Category:Music]]. Need to remove it. - if ( substr($m[1],0,1) == ':') { - # We don't want to keep the ':' - $m[1] = substr( $m[1], 1 ); - } - - $rt = Title::newFromText( $m[1] ); - # Disallow redirects to Special:Userlogout - if ( !is_null($rt) && $rt->isSpecial( 'Userlogout' ) ) { - $rt = NULL; + if( preg_match( '!\[{2}(.*?)(?:\||\]{2})!', $text, $m ) ) { + // Strip preceding colon used to "escape" categories, etc. + // and URL-decode links + if( strpos( $m[1], '%' ) !== false ) { + // Match behavior of inline link parsing here; + // don't interpret + as " " most of the time! + // It might be safe to just use rawurldecode instead, though. + $m[1] = urldecode( ltrim( $m[1], ':' ) ); } + $title = Title::newFromText( $m[1] ); + // Redirects to Special:Userlogout are not permitted + if( $title instanceof Title && !$title->isSpecial( 'Userlogout' ) ) + return $title; } } - return $rt; + return null; } #---------------------------------------------------------------------------- @@ -555,6 +557,12 @@ class Title { } return $wgContLang->getNsText( $this->mNamespace ); } + /** + * Get the DB key with the initial letter case as specified by the user + */ + function getUserCaseDBKey() { + return $this->mUserCaseDBKey; + } /** * Get the namespace text of the subject (rather than talk) page * @return string @@ -988,6 +996,23 @@ class Title { return $this->userCan( $action, false ); } + /** + * Determines if $wgUser is unable to edit this page because it has been protected + * by $wgNamespaceProtection. + * + * @return boolean + */ + public function isNamespaceProtected() { + global $wgNamespaceProtection, $wgUser; + if( isset( $wgNamespaceProtection[ $this->mNamespace ] ) ) { + foreach( (array)$wgNamespaceProtection[ $this->mNamespace ] as $right ) { + if( $right != '' && !$wgUser->isAllowed( $right ) ) + return true; + } + } + return false; + } + /** * Can $wgUser perform $action on this page? * @param string $action action that permission needs to be checked for @@ -995,48 +1020,124 @@ class Title { * @return boolean */ public function userCan( $action, $doExpensiveQueries = true ) { + global $wgUser; + return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array()); + } + + /** + * Can $user perform $action on this page? + * @param string $action action that permission needs to be checked for + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. + * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. + */ + public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) { + $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); + + global $wgContLang; + global $wgLang; + + if ( wfReadOnly() && $action != 'read' ) { + global $wgReadOnly; + $errors[] = array( 'readonlytext', $wgReadOnly ); + } + + global $wgEmailConfirmToEdit, $wgUser; + + if ( $wgEmailConfirmToEdit && !$wgUser->isEmailConfirmed() ) + { + $errors[] = array( 'confirmedittext' ); + } + + if ( $user->isBlockedFrom( $this ) ) { + $block = $user->mBlock; + + // This is from OutputPage::blockedPage + // Copied at r23888 by werdna + + $id = $user->blockedBy(); + $reason = $user->blockedFor(); + $ip = wfGetIP(); + + if ( is_numeric( $id ) ) { + $name = User::whoIs( $id ); + } else { + $name = $id; + } + + $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; + $blockid = $block->mId; + $blockExpiry = $user->mBlock->mExpiry; + $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $wgUser->mBlock->mTimestamp ), true ); + + if ( $blockExpiry == 'infinity' ) { + // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite' + $scBlockExpiryOptions = wfMsg( 'ipboptions' ); + + foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) { + if ( strpos( $option, ':' ) == false ) + continue; + + list ($show, $value) = explode( ":", $option ); + + if ( $value == 'infinite' || $value == 'indefinite' ) { + $blockExpiry = $show; + break; + } + } + } else { + $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); + } + + $intended = $user->mBlock->mAddress; + + $errors[] = array ( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ); + } + + return $errors; + } + + /** + * Can $user perform $action on this page? + * This is an internal function, which checks ONLY that previously checked by userCan (i.e. it leaves out checks on wfReadOnly() and blocks) + * @param string $action action that permission needs to be checked for + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. + * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. + */ + private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) { $fname = 'Title::userCan'; wfProfileIn( $fname ); - global $wgUser, $wgNamespaceProtection; + $errors = array(); - $result = null; - wfRunHooks( 'userCan', array( &$this, &$wgUser, $action, &$result ) ); - if ( $result !== null ) { - wfProfileOut( $fname ); - return $result; + if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { + return $result ? array() : array( array( 'badaccess-group0' ) ); } if( NS_SPECIAL == $this->mNamespace ) { - wfProfileOut( $fname ); - return false; + $errors[] = array('ns-specialprotected'); } - if ( array_key_exists( $this->mNamespace, $wgNamespaceProtection ) ) { - $nsProt = $wgNamespaceProtection[ $this->mNamespace ]; - if ( !is_array($nsProt) ) $nsProt = array($nsProt); - foreach( $nsProt as $right ) { - if( '' != $right && !$wgUser->isAllowed( $right ) ) { - wfProfileOut( $fname ); - return false; - } - } + if ( $this->isNamespaceProtected() ) { + $ns = $this->getNamespace() == NS_MAIN + ? wfMsg( 'nstab-main' ) + : $this->getNsText(); + $errors[] = (NS_MEDIAWIKI == $this->mNamespace + ? array('protectedinterface') + : array( 'namespaceprotected', $ns ) ); } if( $this->mDbkeyform == '_' ) { # FIXME: Is this necessary? Shouldn't be allowed anyway... - wfProfileOut( $fname ); - return false; + $errors[] = array('badaccess-group0'); } # protect css/js subpages of user pages # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working if( $this->isCssJsSubpage() - && !$wgUser->isAllowed('editinterface') - && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) { - wfProfileOut( $fname ); - return false; + && !$user->isAllowed('editinterface') + && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) { + $errors[] = array('customcssjsprotected'); } if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { @@ -1052,9 +1153,11 @@ class Title { if( $cascadingSources > 0 && isset($restrictions[$action]) ) { foreach( $restrictions[$action] as $right ) { $right = ( $right == 'sysop' ) ? 'protect' : $right; - if( '' != $right && !$wgUser->isAllowed( $right ) ) { - wfProfileOut( $fname ); - return false; + if( '' != $right && !$user->isAllowed( $right ) ) { + $pages = ''; + foreach( $cascadingSources as $page ) + $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; + $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); } } } @@ -1065,28 +1168,50 @@ class Title { if ( $right == 'sysop' ) { $right = 'protect'; } - if( '' != $right && !$wgUser->isAllowed( $right ) ) { - wfProfileOut( $fname ); - return false; + if( '' != $right && !$user->isAllowed( $right ) ) { + $errors[] = array( 'protectedpagetext' ); } } - if( $action == 'move' && - !( $this->isMovable() && $wgUser->isAllowed( 'move' ) ) ) { - wfProfileOut( $fname ); - return false; - } - if( $action == 'create' ) { - if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) || - ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) { - wfProfileOut( $fname ); - return false; + if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { + $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); } + } elseif( $action == 'move' && !( $this->isMovable() && $user->isAllowed( 'move' ) ) ) { + $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); + } else if ( !$user->isAllowed( $action ) ) { + $return = null; + $groups = array(); + global $wgGroupPermissions; + foreach( $wgGroupPermissions as $key => $value ) { + if( isset( $value[$action] ) && $value[$action] == true ) { + $groupName = User::getGroupName( $key ); + $groupPage = User::getGroupPage( $key ); + if( $groupPage ) { + $skin = $user->getSkin(); + $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]'; + } else { + $groups[] = $groupName; + } + } + } + $n = count( $groups ); + $groups = implode( ', ', $groups ); + switch( $n ) { + case 0: + case 1: + case 2: + $return = array( "badaccess-group$n", $groups ); + break; + default: + $return = array( 'badaccess-groups', $groups ); + } + $errors[] = $return; } wfProfileOut( $fname ); - return true; + return $errors; } /** @@ -1141,7 +1266,7 @@ class Title { return $result; } - if( $wgUser->isAllowed('read') ) { + if( $wgUser->isAllowed( 'read' ) ) { return true; } else { global $wgWhitelistRead; @@ -1153,19 +1278,35 @@ class Title { if( $this->isSpecial( 'Userlogin' ) || $this->isSpecial( 'Resetpass' ) ) { return true; } - - /** some pages are explicitly allowed */ + + /** + * Check for explicit whitelisting + */ $name = $this->getPrefixedText(); - if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead ) ) { + if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead, true ) ) return true; - } - - # Compatibility with old settings + + /** + * Old settings might have the title prefixed with + * a colon for main-namespace pages + */ if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) { - if( in_array( ':' . $name, $wgWhitelistRead ) ) { + if( in_array( ':' . $name, $wgWhitelistRead ) ) return true; - } } + + /** + * If it's a special page, ditch the subpage bit + * and check again + */ + if( $this->getNamespace() == NS_SPECIAL ) { + $name = $this->getText(); + list( $name, /* $subpage */) = SpecialPage::resolveAliasWithSubpage( $name ); + $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); + if( in_array( $pure, $wgWhitelistRead, true ) ) + return true; + } + } return false; } @@ -1191,6 +1332,17 @@ class Title { return false; } } + + /** + * Could this page contain custom CSS or JavaScript, based + * on the title? + * + * @return bool + */ + public function isCssOrJsPage() { + return $this->mNamespace == NS_MEDIAWIKI + && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; + } /** * Is this a .css or .js subpage of a user page? @@ -1251,7 +1403,7 @@ class Title { * @return bool If the page is subject to cascading restrictions. */ public function isCascadeProtected() { - list( $sources, $restrictions ) = $this->getCascadeProtectionSources( false ); + list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); return ( $sources > 0 ); } @@ -1749,6 +1901,7 @@ class Title { * Don't force it for interwikis, since the other * site might be case-sensitive. */ + $this->mUserCaseDBKey = $dbkey; if( $wgCapitalLinks && $this->mInterwiki == '') { $dbkey = $wgContLang->ucfirst( $dbkey ); } @@ -1763,7 +1916,14 @@ class Title { $this->mNamespace != NS_MAIN ) { return false; } - + // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles. + // IP names are not allowed for accounts, and can only be referring to + // edits from the IP. Given '::' abbreviations and caps/lowercaps, + // there are numerous ways to present the same IP. Having sp:contribs scan + // them all is silly and having some show the edits and others not is + // inconsistent. Same for talk/userpages. Keep them normalized instead. + $dbkey = ($this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK) ? + IP::sanitizeIP( $dbkey ) : $dbkey; // Any remaining initial :s are illegal. if ( $dbkey !== '' && ':' == $dbkey{0} ) { return false; @@ -2270,6 +2430,16 @@ class Title { # Return true if there was no history return $row === false; } + + /** + * Can this title be added to a user's watchlist? + * + * @return bool + */ + public function isWatchable() { + return !$this->isExternal() + && Namespace::isWatchable( $this->getNamespace() ); + } /** * Get categories to which this Title belongs and return an array of @@ -2394,6 +2564,15 @@ class Title { && $this->getNamespace() == $title->getNamespace() && $this->getDbkey() === $title->getDbkey(); } + + /** + * Return a string representation of this title + * + * @return string + */ + public function __toString() { + return $this->getPrefixedText(); + } /** * Check if page exists @@ -2404,14 +2583,15 @@ class Title { } /** - * Should a link should be displayed as a known link, just based on its title? + * Do we know that this title definitely exists, or should we otherwise + * consider that it exists? * - * Currently, a self-link with a fragment and special pages are in - * this category. Special pages never exist in the database. + * @return bool */ public function isAlwaysKnown() { - return $this->isExternal() || ( 0 == $this->mNamespace && "" == $this->mDbkeyform ) - || NS_SPECIAL == $this->mNamespace; + return $this->isExternal() + || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' ) + || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $this->mDbkeyform ) ); } /** @@ -2551,4 +2731,4 @@ class Title { } -?> + diff --git a/includes/User.php b/includes/User.php index 4ecd49de..51b0b2ec 100644 --- a/includes/User.php +++ b/includes/User.php @@ -11,9 +11,7 @@ define( 'USER_TOKEN_LENGTH', 32 ); define( 'MW_USER_VERSION', 5 ); # Some punctuation to prevent editing from broken text-mangling proxies. -# FIXME: this is embedded unescaped into HTML attributes in various -# places, so we can't safely include ' or " even though we really should. -define( 'EDIT_TOKEN_SUFFIX', '\\' ); +define( 'EDIT_TOKEN_SUFFIX', '+\\' ); /** * Thrown by User::setPassword() on error @@ -36,8 +34,8 @@ class PasswordError extends MWException { class User { /** - * A list of default user toggles, i.e. boolean user preferences that are - * displayed by Special:Preferences as checkboxes. This list can be + * A list of default user toggles, i.e. boolean user preferences that are + * displayed by Special:Preferences as checkboxes. This list can be * extended via the UserToggles hook or $wgContLang->getExtraUserToggles(). */ static public $mToggles = array( @@ -82,7 +80,7 @@ class User { /** * List of member variables which are saved to the shared cache (memcached). - * Any operation which changes the corresponding database fields must + * Any operation which changes the corresponding database fields must * call a cache-clearing function. */ static $mCacheVars = array( @@ -109,8 +107,8 @@ class User { /** * The cache variable declarations */ - var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, - $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, + var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime, + $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated, $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups; /** @@ -135,7 +133,7 @@ class User { var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights, $mBlockreason, $mBlock, $mEffectiveGroups; - /** + /** * Lightweight constructor for anonymous user * Use the User::newFrom* factory functions for other kinds of users */ @@ -190,7 +188,7 @@ class User { if ( $this->mId == 0 ) { $this->loadDefaults(); return false; - } + } # Try cache $key = wfMemcKey( 'user', 'id', $this->mId ); @@ -199,7 +197,7 @@ class User { # Object is expired, load from DB $data = false; } - + if ( !$data ) { wfDebug( "Cache miss for user {$this->mId}\n" ); # Load from DB @@ -229,16 +227,16 @@ class User { * Static factory method for creation from username. * * This is slightly less efficient than newFromId(), so use newFromId() if - * you have both an ID and a name handy. + * you have both an ID and a name handy. * * @param string $name Username, validated by Title:newFromText() - * @param mixed $validate Validate username. Takes the same parameters as - * User::getCanonicalName(), except that true is accepted as an alias + * @param mixed $validate Validate username. Takes the same parameters as + * User::getCanonicalName(), except that true is accepted as an alias * for 'valid', for BC. - * - * @return User object, or null if the username is invalid. If the username + * + * @return User object, or null if the username is invalid. If the username * is not present in the database, the result will be a user object with - * a name, zero user ID and default settings. + * a name, zero user ID and default settings. * @static */ static function newFromName( $name, $validate = 'valid' ) { @@ -287,7 +285,7 @@ class User { return null; } } - + /** * Create a new user object using data from session or cookies. If the * login credentials are invalid, the result is an anonymous user. @@ -313,14 +311,14 @@ class User { } /** - * Get real username given an id. - * @param integer $id Database user id - * @return string Realname of a user - * @static + * Get the real name of a user given their identifier + * + * @param int $id Database user id + * @return string Real name of a user */ static function whoIsReal( $id ) { $dbr = wfGetDB( DB_SLAVE ); - return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), 'User::whoIsReal' ); + return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ), __METHOD__ ); } /** @@ -350,9 +348,9 @@ class User { * * This function exists for username validation, in order to reject * usernames which are similar in form to IP addresses. Strings such - * as 300.300.300.300 will return true because it looks like an IP + * as 300.300.300.300 will return true because it looks like an IP * address, despite not being strictly valid. - * + * * We match \d{1,3}\.\d{1,3}\.\d{1,3}\.xxx as an anonymous IP * address because the usemod software would "cloak" anonymous IP * addresses like this, if we allowed accounts like this to be created @@ -376,8 +374,8 @@ class User { * Check if $name is an IPv6 IP. */ static function isIPv6($name) { - /* - * if it has any non-valid characters, it can't be a valid IPv6 + /* + * if it has any non-valid characters, it can't be a valid IPv6 * address. */ if (preg_match("/[^:a-fA-F0-9]/", $name)) @@ -422,7 +420,7 @@ class User { || $parsed->getNamespace() || strcmp( $name, $parsed->getPrefixedText() ) ) return false; - + // Check an additional blacklist of troublemaker characters. // Should these be merged into the title char list? $unicodeBlacklist = '/[' . @@ -436,10 +434,10 @@ class User { if( preg_match( $unicodeBlacklist, $name ) ) { return false; } - + return true; } - + /** * Usernames which fail to pass this function will be blocked * from user login and new account registrations, but may be used @@ -454,13 +452,13 @@ class User { static function isUsableName( $name ) { global $wgReservedUsernames; return - // Must be a usable username, obviously ;) + // Must be a valid username, obviously ;) self::isValidUserName( $name ) && - + // Certain names may be reserved for batch processes. !in_array( $name, $wgReservedUsernames ); } - + /** * Usernames which fail to pass this function will be blocked * from new account registrations, but may be used internally @@ -477,29 +475,33 @@ class User { static function isCreatableName( $name ) { return self::isUsableName( $name ) && - + // Registration-time character blacklisting... strpos( $name, '@' ) === false; } /** - * Is the input a valid password? + * Is the input a valid password for this user? * - * @param string $password + * @param string $password Desired password * @return bool */ function isValidPassword( $password ) { global $wgMinimalPasswordLength, $wgContLang; $result = null; - if( !wfRunHooks( 'isValidPassword', array( $password, &$result ) ) ) return $result; - if ($result === false) return false; - return (strlen( $password ) >= $wgMinimalPasswordLength) && - ($wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName )); + if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) ) + return $result; + if( $result === false ) + return false; + + // Password needs to be long enough, and can't be the same as the username + return strlen( $password ) >= $wgMinimalPasswordLength + && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName ); } /** - * Does the string match roughly an email address ? + * Does a string look like an email address? * * There used to be a regular expression here, it got removed because it * rejected valid addresses. Actually just check if there is '@' somewhere @@ -508,16 +510,14 @@ class User { * @todo Check for RFC 2822 compilance (bug 959) * * @param string $addr email address - * @static * @return bool */ - static function isValidEmailAddr ( $addr ) { - return ( trim( $addr ) != '' ) && - (false !== strpos( $addr, '@' ) ); + public static function isValidEmailAddr( $addr ) { + return strpos( $addr, '@' ) !== false; } /** - * Given unvalidated user input, return a canonical username, or false if + * Given unvalidated user input, return a canonical username, or false if * the username is invalid. * @param string $name * @param mixed $validate Type of validation to use: @@ -531,6 +531,12 @@ class User { global $wgContLang; $name = $wgContLang->ucfirst( $name ); + # Reject names containing '#'; these will be cleaned up + # with title normalisation, but then it's too late to + # check elsewhere + if( strpos( $name, '#' ) !== false ) + return false; + # Clean up name according to title rules $t = Title::newFromText( $name ); if( is_null( $t ) ) { @@ -570,7 +576,7 @@ class User { * Count the number of edits of a user * * It should not be static and some day should be merged as proper member function / deprecated -- domas - * + * * @param int $uid The user ID to check * @return int * @static @@ -627,7 +633,7 @@ class User { } /** - * Set cached properties to default. Note: this no longer clears + * Set cached properties to default. Note: this no longer clears * uncached lazy-initialised properties. The constructor does that instead. * * @private @@ -660,7 +666,7 @@ class User { wfProfileOut( __METHOD__ ); } - + /** * Initialise php session * @deprecated use wfSetupSession() @@ -707,7 +713,7 @@ class User { # Not a valid ID, loadFromId has switched the object to anon for us return false; } - + if ( isset( $_SESSION['wsToken'] ) ) { $passwordCorrect = $_SESSION['wsToken'] == $this->mToken; $from = 'session'; @@ -721,6 +727,7 @@ class User { } if ( ( $sName == $this->mName ) && $passwordCorrect ) { + $_SESSION['wsToken'] = $this->mToken; wfDebug( "Logged in from $from\n" ); return true; } else { @@ -730,11 +737,11 @@ class User { return false; } } - + /** * Load user and user_group data from the database * $this->mId must be set, this is how the user is identified. - * + * * @return true if the user exists, false if the user is anonymous * @private */ @@ -766,7 +773,7 @@ class User { $this->mEmailToken = $s->user_email_token; $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $s->user_email_token_expires ); $this->mRegistration = wfTimestampOrNull( TS_MW, $s->user_registration ); - $this->mEditCount = $s->user_editcount; + $this->mEditCount = $s->user_editcount; $this->getEditCount(); // revalidation for nulls # Load group data @@ -788,9 +795,9 @@ class User { } /** - * Clear various cached data stored in this object. - * @param string $reloadFrom Reload user and user_groups table data from a - * given source. May be "name", "id", "defaults", "session" or false for + * Clear various cached data stored in this object. + * @param string $reloadFrom Reload user and user_groups table data from a + * given source. May be "name", "id", "defaults", "session" or false for * no reload. */ function clearInstanceCache( $reloadFrom = false ) { @@ -884,7 +891,7 @@ class User { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": checking...\n" ); - $this->mBlockedby = 0; + $this->mBlockedby = 0; $this->mHideName = 0; $ip = wfGetIP(); @@ -1112,9 +1119,16 @@ class User { /** * Get the user ID. Returns 0 if the user is anonymous or nonexistent. */ - function getID() { - $this->load(); - return $this->mId; + function getID() { + if( $this->mId === null and $this->mName !== null + and User::isIP( $this->mName ) ) { + // Special case, we know the user is anonymous + return 0; + } elseif( $this->mId === null ) { + // Don't load if this was initialized from an ID + $this->load(); + } + return $this->mId; } /** @@ -1144,12 +1158,12 @@ class User { } /** - * Set the user name. + * Set the user name. * - * This does not reload fields from the database according to the given + * This does not reload fields from the database according to the given * name. Rather, it is used to create a temporary "nonexistent user" for - * later addition to the database. It can also be used to set the IP - * address for an anonymous user to something other than the current + * later addition to the database. It can also be used to set the IP + * address for an anonymous user to something other than the current * remote IP. * * User::newFromName() has rougly the same function, when the named user @@ -1211,7 +1225,7 @@ class User { return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL())); } - + /** * Perform a user_newtalk check on current slaves; if the memcached data * is funky we don't want newtalk state to get stuck on save, as that's @@ -1311,7 +1325,7 @@ class User { $this->invalidateCache(); } } - + /** * Generate a current or new-future timestamp to be stored in the * user_touched field when we update things. @@ -1320,7 +1334,7 @@ class User { global $wgClockSkewFudge; return wfTimestamp( TS_MW, time() + $wgClockSkewFudge ); } - + /** * Clear user data from memcached. * Use after applying fun updates to the database; caller's @@ -1344,13 +1358,13 @@ class User { $this->load(); if( $this->mId ) { $this->mTouched = self::newTouchedTimestamp(); - + $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'user', array( 'user_touched' => $dbw->timestamp( $this->mTouched ) ), array( 'user_id' => $this->mId ), __METHOD__ ); - + $this->clearSharedCache(); } } @@ -1362,7 +1376,8 @@ class User { /** * Encrypt a password. - * It can eventuall salt a password @see User::addSalt() + * It can eventually salt a password. + * @see User::addSalt() * @param string $p clear Password. * @return string Encrypted password. */ @@ -1387,12 +1402,12 @@ class User { */ function setPassword( $str ) { global $wgAuth; - + if( $str !== null ) { if( !$wgAuth->allowPasswordChange() ) { throw new PasswordError( wfMsg( 'password-change-forbidden' ) ); } - + if( !$this->isValidPassword( $str ) ) { global $wgMinimalPasswordLength; throw new PasswordError( wfMsg( 'passwordtooshort', @@ -1403,7 +1418,7 @@ class User { if( !$wgAuth->setPassword( $this, $str ) ) { throw new PasswordError( wfMsg( 'externaldberror' ) ); } - + $this->setInternalPassword( $str ); return true; @@ -1418,7 +1433,7 @@ class User { function setInternalPassword( $str ) { $this->load(); $this->setToken(); - + if( $str === null ) { // Save an invalid hash... $this->mPassword = ''; @@ -1480,7 +1495,7 @@ class User { $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgPasswordReminderResendTime * 3600; return time() < $expiry; } - + function getEmail() { $this->load(); return $this->mEmail; @@ -1529,7 +1544,7 @@ class User { } /** - * Get the user's date preference, including some important migration for + * Get the user's date preference, including some important migration for * old user rows. */ function getDatePreference() { @@ -1552,7 +1567,7 @@ class User { function getBoolOption( $oname ) { return (bool)$this->getOption( $oname ); } - + /** * Get an option as an integer value from the source string. * @param string $oname The option to check @@ -1587,6 +1602,7 @@ class User { function getRights() { if ( is_null( $this->mRights ) ) { $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() ); + wfRunHooks( 'UserGetRights', array( $this, &$this->mRights ) ); } return $this->mRights; } @@ -1615,7 +1631,7 @@ class User { $this->mEffectiveGroups[] = '*'; if( $this->mId ) { $this->mEffectiveGroups[] = 'user'; - + global $wgAutoConfirmAge, $wgAutoConfirmCount; $accountAge = time() - wfTimestampOrNull( TS_UNIX, $this->mRegistration ); @@ -1632,25 +1648,27 @@ class User { $this->mEffectiveGroups[] = 'emailconfirmed'; } } + # Hook for additional groups + wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) ); } } return $this->mEffectiveGroups; } - + /* Return the edit count for the user. This is where User::edits should have been */ function getEditCount() { if ($this->mId) { if ( !isset( $this->mEditCount ) ) { /* Populate the count, if it has not been populated yet */ $this->mEditCount = User::edits($this->mId); - } + } return $this->mEditCount; } else { /* nil */ return null; } } - + /** * Add the user to the given group. * This takes immediate effect. @@ -1704,7 +1722,11 @@ class User { * @return bool */ function isLoggedIn() { - return( $this->getID() != 0 ); + if( $this->mId === null and $this->mName !== null ) { + // Special-case optimization + return !self::isIP( $this->mName ); + } + return $this->getID() != 0; } /** @@ -1955,7 +1977,7 @@ class User { $this->load(); if ( wfReadOnly() ) { return; } if ( 0 == $this->mId ) { return; } - + $this->mTouched = self::newTouchedTimestamp(); $dbw = wfGetDB( DB_MASTER ); @@ -2044,7 +2066,7 @@ class User { } return $newUser; } - + /** * Add an existing user object to the database */ @@ -2132,6 +2154,8 @@ class User { // extra options or other effects on the parser cache. wfRunHooks( 'PageRenderingHash', array( &$confstr ) ); + // Make it a valid memcached key fragment + $confstr = str_replace( ' ', '_', $confstr ); $this->mHash = $confstr; return $confstr; } @@ -2141,6 +2165,17 @@ class User { return $this->mBlock && $this->mBlock->mCreateAccount; } + /** + * Determine if the user is blocked from using Special:Emailuser. + * + * @public + * @return boolean + */ + function isBlockedFromEmailuser() { + $this->getBlockedStatus(); + return $this->mBlock && $this->mBlock->mBlockEmail; + } + function isAllowedToCreateAccount() { return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); } @@ -2231,7 +2266,7 @@ class User { } return false; } - + /** * Check if the given clear-text password matches the temporary password * sent by e-mail for password reset operations. @@ -2254,16 +2289,20 @@ class User { * @public */ function editToken( $salt = '' ) { - if( !isset( $_SESSION['wsEditToken'] ) ) { - $token = $this->generateToken(); - $_SESSION['wsEditToken'] = $token; + if ( $this->isAnon() ) { + return EDIT_TOKEN_SUFFIX; } else { - $token = $_SESSION['wsEditToken']; - } - if( is_array( $salt ) ) { - $salt = implode( '|', $salt ); + if( !isset( $_SESSION['wsEditToken'] ) ) { + $token = $this->generateToken(); + $_SESSION['wsEditToken'] = $token; + } else { + $token = $_SESSION['wsEditToken']; + } + if( is_array( $salt ) ) { + $salt = implode( '|', $salt ); + } + return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; } - return md5( $token . $salt ) . EDIT_TOKEN_SUFFIX; } /** @@ -2288,7 +2327,6 @@ class User { * @public */ function matchEditToken( $val, $salt = '' ) { - global $wgMemc; $sessionToken = $this->editToken( $salt ); if ( $val != $sessionToken ) { wfDebug( "User::matchEditToken: broken session data\n" ); @@ -2296,6 +2334,14 @@ class User { return $val == $sessionToken; } + /** + * Check whether the edit token is fine except for the suffix + */ + function matchEditTokenNoSuffix( $val, $salt = '' ) { + $sessionToken = $this->editToken( $salt ); + return substr( $sessionToken, 0, 32 ) == substr( $val, 0, 32 ); + } + /** * Generate a new e-mail confirmation token and send a confirmation * mail to the user's given address. @@ -2433,7 +2479,7 @@ class User { return $confirmed; } } - + /** * Return true if there is an outstanding request for e-mail confirmation. * @return bool @@ -2446,6 +2492,18 @@ class User { $this->mEmailTokenExpires > wfTimestamp(); } + /** + * Get the timestamp of account creation, or false for + * non-existent/anonymous user accounts + * + * @return mixed + */ + public function getRegistration() { + return $this->mId > 0 + ? $this->mRegistration + : false; + } + /** * @param array $groups list of groups * @return array list of permission key names for given groups combined @@ -2469,7 +2527,8 @@ class User { * @static */ static function getGroupName( $group ) { - MessageCache::loadAllMessages(); + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "group-$group"; $name = wfMsg( $key ); return $name == '' || wfEmptyMsg( $key, $name ) @@ -2483,7 +2542,8 @@ class User { * @static */ static function getGroupMember( $group ) { - MessageCache::loadAllMessages(); + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $key = "group-$group-member"; $name = wfMsg( $key ); return $name == '' || wfEmptyMsg( $key, $name ) @@ -2503,7 +2563,22 @@ class User { global $wgGroupPermissions; return array_diff( array_keys( $wgGroupPermissions ), - array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ) ); + self::getImplicitGroups() + ); + } + + /** + * Get a list of implicit groups + * + * @return array + */ + public static function getImplicitGroups() { + static $groups = null; + if( !is_array( $groups ) ) { + $groups = array( '*', 'user', 'autoconfirmed', 'emailconfirmed' ); + wfRunHooks( 'UserGetImplicitGroups', array( &$groups ) ); + } + return $groups; } /** @@ -2513,7 +2588,8 @@ class User { * @return mixed */ static function getGroupPage( $group ) { - MessageCache::loadAllMessages(); + global $wgMessageCache; + $wgMessageCache->loadAllMessages(); $page = wfMsgForContent( 'grouppage-' . $group ); if( !wfEmptyMsg( 'grouppage-' . $group, $page ) ) { $title = Title::newFromText( $page ); @@ -2563,7 +2639,7 @@ class User { return $text; } } - + /** * Increment the user's edit-count field. * Will have no effect for anonymous users. @@ -2575,7 +2651,7 @@ class User { array( 'user_editcount=user_editcount+1' ), array( 'user_id' => $this->getId() ), __METHOD__ ); - + // Lazy initialization check... if( $dbw->affectedRows() == 0 ) { // Pull from a slave to be less cruel to servers @@ -2585,7 +2661,7 @@ class User { 'COUNT(rev_user)', array( 'rev_user' => $this->getId() ), __METHOD__ ); - + // Now here's a goddamn hack... if( $dbr !== $dbw ) { // If we actually have a slave server, the count is @@ -2597,7 +2673,7 @@ class User { // count we just read includes the revision that was // just added in the working transaction. } - + $dbw->update( 'user', array( 'user_editcount' => $count ), array( 'user_id' => $this->getId() ), @@ -2609,4 +2685,4 @@ class User { } } -?> + diff --git a/includes/UserMailer.php b/includes/UserMailer.php index 9f5f178c..835dd310 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -72,6 +72,22 @@ class MailAddress { } } +function send_mail($mailer, $dest, $headers, $body) +{ + $mailResult =& $mailer->send($dest, $headers, $body); + + # Based on the result return an error string, + if ($mailResult === true) { + return ''; + } elseif (is_object($mailResult)) { + wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" ); + return $mailResult->getMessage(); + } else { + wfDebug( "PEAR::Mail failed, unknown error result\n" ); + return 'Mail object return unknown error.'; + } +} + /** * This function will perform a direct (authenticated) login to * a SMTP Server to use for mail relaying if 'wgSMTP' specifies an @@ -85,16 +101,30 @@ class MailAddress { * @param $replyto String: optional reply-to email (default: null). */ function userMailer( $to, $from, $subject, $body, $replyto=null ) { - global $wgUser, $wgSMTP, $wgOutputEncoding, $wgErrorString; + global $wgSMTP, $wgOutputEncoding, $wgErrorString, $wgEnotifImpersonal; + global $wgEnotifMaxRecips; if (is_array( $wgSMTP )) { require_once( 'Mail.php' ); - $timestamp = time(); - $dest = $to->address; + $msgid = str_replace(" ", "_", microtime()); + if (function_exists('posix_getpid')) + $msgid .= '.' . posix_getpid(); + + if (is_array($to)) { + $dest = array(); + foreach ($to as $u) + $dest[] = $u->address; + } else + $dest = $to->address; $headers['From'] = $from->toString(); - $headers['To'] = $to->toString(); + + if ($wgEnotifImpersonal) + $headers['To'] = 'undisclosed-recipients:;'; + else + $headers['To'] = $to->toString(); + if ( $replyto ) { $headers['Reply-To'] = $replyto->toString(); } @@ -103,7 +133,7 @@ function userMailer( $to, $from, $subject, $body, $replyto=null ) { $headers['MIME-Version'] = '1.0'; $headers['Content-type'] = 'text/plain; charset='.$wgOutputEncoding; $headers['Content-transfer-encoding'] = '8bit'; - $headers['Message-ID'] = "<{$timestamp}" . $wgUser->getName() . '@' . $wgSMTP['IDHost'] . '>'; // FIXME + $headers['Message-ID'] = "<$msgid@" . $wgSMTP['IDHost'] . '>'; // FIXME $headers['X-Mailer'] = 'MediaWiki mailer'; // Create the mail object using the Mail::factory method @@ -114,18 +144,16 @@ function userMailer( $to, $from, $subject, $body, $replyto=null ) { } wfDebug( "Sending mail via PEAR::Mail to $dest\n" ); - $mailResult =& $mail_object->send($dest, $headers, $body); + if (is_array($dest)) { + $chunks = array_chunk($dest, $wgEnotifMaxRecips); + foreach ($chunks as $chunk) { + $e = send_mail($mail_object, $chunk, $headers, $body); + if ($e != '') + return $e; + } + } else + return $mail_object->send($dest, $headers, $body); - # Based on the result return an error string, - if ($mailResult === true) { - return ''; - } elseif (is_object($mailResult)) { - wfDebug( "PEAR::Mail failed: " . $mailResult->getMessage() . "\n" ); - return $mailResult->getMessage(); - } else { - wfDebug( "PEAR::Mail failed, unknown error result\n" ); - return 'Mail object return unknown error.'; - } } else { # In the following $headers = expression we removed "Reply-To: {$from}\r\n" , because it is treated differently # (fifth parameter of the PHP mail function, see some lines below) @@ -148,21 +176,37 @@ function userMailer( $to, $from, $subject, $body, $replyto=null ) { $headers .= "{$endl}Reply-To: " . $replyto->toString(); } - $dest = $to->toString(); - $wgErrorString = ''; set_error_handler( 'mailErrorHandler' ); - wfDebug( "Sending mail via internal mail() function to $dest\n" ); - mail( $dest, wfQuotedPrintable( $subject ), $body, $headers ); + wfDebug( "Sending mail via internal mail() function\n" ); + + if (function_exists('mail')) + if (is_array($to)) + foreach ($to as $recip) + $sent = mail( $recip->toString(), wfQuotedPrintable( $subject ), $body, $headers ); + else + $sent = mail( $to->toString(), wfQuotedPrintable( $subject ), $body, $headers ); + else + $wgErrorString = 'PHP is not configured to send mail'; + + restore_error_handler(); if ( $wgErrorString ) { wfDebug( "Error sending mail: $wgErrorString\n" ); + return $wgErrorString; + } elseif (! $sent) { + //mail function only tells if there's an error + wfDebug( "Error sending mail\n" ); + return 'mailer error'; + } else { + return ''; } - return $wgErrorString; } } + + /** * Get the mail error message in global $wgErrorString * @@ -204,6 +248,27 @@ class EmailNotification { /**@}}*/ + function notifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid = false) { + global $wgEnotifUseJobQ; + + if( $title->getNamespace() < 0 ) + return; + + if ($wgEnotifUseJobQ) { + $params = array( + "editor" => $editor->getName(), + "timestamp" => $timestamp, + "summary" => $summary, + "minorEdit" => $minorEdit, + "oldid" => $oldid); + $job = new EnotifNotifyJob( $title, $params ); + $job->insert(); + } else { + $this->actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid); + } + + } + /** * @todo document * @param $title Title object @@ -212,11 +277,12 @@ class EmailNotification { * @param $minorEdit * @param $oldid (default: false) */ - function notifyOnPageChange(&$title, $timestamp, $summary, $minorEdit, $oldid=false) { + function actuallyNotifyOnPageChange($editor, &$title, $timestamp, $summary, $minorEdit, $oldid=false) { # we use $wgEmergencyContact as sender's address - global $wgUser, $wgEnotifWatchlist; + global $wgEnotifWatchlist; global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker; + global $wgEnotifImpersonal; $fname = 'UserMailer::notifyOnPageChange'; wfProfileIn( $fname ); @@ -234,18 +300,20 @@ class EmailNotification { $this->summary = $summary; $this->minorEdit = $minorEdit; $this->oldid = $oldid; - $this->composeCommonMailtext(); + $this->composeCommonMailtext($editor); + + $impersonals = array(); if ( (!$minorEdit || $wgEnotifMinorEdits) ) { if( $wgEnotifWatchlist ) { // Send updates to watchers other than the current editor - $userCondition = 'wl_user <> ' . intval( $wgUser->getId() ); + $userCondition = 'wl_user <> ' . intval( $editor->getId() ); } elseif( $wgEnotifUserTalk && $title->getNamespace() == NS_USER_TALK ) { $targetUser = User::newFromName( $title->getText() ); if( is_null( $targetUser ) ) { wfDebug( "$fname: user-talk-only mode; no such user\n" ); $userCondition = false; - } elseif( $targetUser->getId() == $wgUser->getId() ) { + } elseif( $targetUser->getId() == $editor->getId() ) { wfDebug( "$fname: user-talk-only mode; editor is target user\n" ); $userCondition = false; } else { @@ -287,7 +355,10 @@ class EmailNotification { && ($watchingUser->isEmailConfirmed() ) ) { # ... adjust remaining text and page edit time placeholders # which needs to be personalized for each user - $this->composeAndSendPersonalisedMail( $watchingUser ); + if ($wgEnotifImpersonal) + $impersonals[] = $watchingUser; + else + $this->composeAndSendPersonalisedMail( $watchingUser ); } # if the watching user has an email address in the preferences } @@ -298,9 +369,14 @@ class EmailNotification { global $wgUsersNotifedOnAllChanges; foreach ( $wgUsersNotifedOnAllChanges as $name ) { $user = User::newFromName( $name ); - $this->composeAndSendPersonalisedMail( $user ); + if ($wgEnotifImpersonal) + $impersonals[] = $user; + else + $this->composeAndSendPersonalisedMail( $user ); } + $this->composeAndSendImpersonalMail($impersonals); + if ( $wgShowUpdatedMarker || $wgEnotifWatchlist ) { # mark the changed watch-listed page with a timestamp, so that the page is # listed with an "updated since your last visit" icon in the watch list, ... @@ -311,19 +387,22 @@ class EmailNotification { ), array( /* WHERE */ 'wl_title' => $title->getDBkey(), 'wl_namespace' => $title->getNamespace(), + 'wl_notificationtimestamp IS NULL' ), 'UserMailer::NotifyOnChange' ); # FIXME what do we do on failure ? } + wfProfileOut( $fname ); } # function NotifyOnChange /** * @private */ - function composeCommonMailtext() { - global $wgUser, $wgEmergencyContact, $wgNoReplyAddress; + function composeCommonMailtext($editor) { + global $wgEmergencyContact, $wgNoReplyAddress; global $wgEnotifFromEditor, $wgEnotifRevealEditorAddress; + global $wgEnotifImpersonal; $summary = ($this->summary == '') ? ' - ' : $this->summary; $medit = ($this->minorEdit) ? wfMsg( 'minoredit' ) : ''; @@ -353,6 +432,14 @@ class EmailNotification { $keys['$CHANGEDORCREATED'] = wfMsgForContent( 'created' ); } + if ($wgEnotifImpersonal && $this->oldid) + /* + * For impersonal mail, show a diff link to the last + * revision. + */ + $keys['$NEWPAGE'] = wfMsgForContent('enotif_lastdiff', + $this->title->getFullURL("oldid={$this->oldid}&diff=prev")); + $body = strtr( $body, $keys ); $pagetitle = $this->title->getPrefixedText(); $keys['$PAGETITLE'] = $pagetitle; @@ -366,12 +453,12 @@ class EmailNotification { # Reveal the page editor's address as REPLY-TO address only if # the user has not opted-out and the option is enabled at the # global configuration level. - $name = $wgUser->getName(); + $name = $editor->getName(); $adminAddress = new MailAddress( $wgEmergencyContact, 'WikiAdmin' ); - $editorAddress = new MailAddress( $wgUser ); + $editorAddress = new MailAddress( $editor ); if( $wgEnotifRevealEditorAddress - && ( $wgUser->getEmail() != '' ) - && $wgUser->getOption( 'enotifrevealaddr' ) ) { + && ( $editor->getEmail() != '' ) + && $editor->getOption( 'enotifrevealaddr' ) ) { if( $wgEnotifFromEditor ) { $from = $editorAddress; } else { @@ -383,10 +470,11 @@ class EmailNotification { $replyto = new MailAddress( $wgNoReplyAddress ); } - if( $wgUser->isIP( $name ) ) { + if( $editor->isIP( $name ) ) { #real anon (user:xxx.xxx.xxx.xxx) - $subject = str_replace('$PAGEEDITOR', 'anonymous user '. $name, $subject); - $keys['$PAGEEDITOR'] = 'anonymous user ' . $name; + $utext = wfMsgForContent('enotif_anon_editor', $name); + $subject = str_replace('$PAGEEDITOR', $utext, $subject); + $keys['$PAGEEDITOR'] = $utext; $keys['$PAGEEDITOR_EMAIL'] = wfMsgForContent( 'noemailtitle' ); } else { $subject = str_replace('$PAGEEDITOR', $name, $subject); @@ -394,7 +482,7 @@ class EmailNotification { $emailPage = SpecialPage::getSafeTitleFor( 'Emailuser', $name ); $keys['$PAGEEDITOR_EMAIL'] = $emailPage->getFullUrl(); } - $userPage = $wgUser->getUserPage(); + $userPage = $editor->getUserPage(); $keys['$PAGEEDITOR_WIKI'] = $userPage->getFullUrl(); $body = strtr( $body, $keys ); $body = wordwrap( $body, 72 ); @@ -406,8 +494,6 @@ class EmailNotification { $this->body = $body; } - - /** * Does the per-user customizations to a notification e-mail (name, * timestamp in proper timezone, etc) and sends it out. @@ -435,9 +521,32 @@ class EmailNotification { $wgLang->timeanddate( $this->timestamp, true, false, $timecorrection ), $body); - $error = userMailer( $to, $this->from, $this->subject, $body, $this->replyto ); - return ($error == ''); + return userMailer($to, $this->from, $this->subject, $body, $this->replyto); + } + + /** + * Same as composeAndSendPersonalisedMail but does impersonal mail + * suitable for bulk mailing. Takes an array of users. + */ + function composeAndSendImpersonalMail($users) { + global $wgLang; + + if (empty($users)) + return; + + $to = array(); + foreach ($users as $user) + $to[] = new MailAddress($user); + + $body = str_replace( + array( '$WATCHINGUSERNAME', + '$PAGEEDITDATE'), + array( wfMsgForContent('enotif_impersonal_salutation'), + $wgLang->timeanddate($this->timestamp, true, false, false)), + $this->body); + + return userMailer($to, $this->from, $this->subject, $body, $this->replyto); } } # end of class EmailNotification -?> + diff --git a/includes/Utf8Case.php b/includes/Utf8Case.php index 279c0e32..1d3af41c 100644 --- a/includes/Utf8Case.php +++ b/includes/Utf8Case.php @@ -1502,4 +1502,4 @@ $wikiLowerChars = array ( "\xf0\x90\x90\xa5" => "\xf0\x90\x91\x8d" ); -?> + diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index b0376e3d..e4de67c8 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -14,10 +14,10 @@ class WatchedItem { * @todo document * @access private */ - function &fromUserTitle( &$user, &$title ) { + static function fromUserTitle( $user, $title ) { $wl = new WatchedItem; - $wl->mUser =& $user; - $wl->mTitle =& $title; + $wl->mUser = $user; + $wl->mTitle = $title; $wl->id = $user->getId(); # Patch (also) for email notification on page changes T.Gries/M.Arndt 11.09.2004 # TG patch: here we do not consider pages and their talk pages equivalent - why should we ? @@ -116,18 +116,13 @@ class WatchedItem { * * @param Title $ot Page title to duplicate entries from, if present * @param Title $nt Page title to add watches on - * @static */ - function duplicateEntries( $ot, $nt ) { + static function duplicateEntries( $ot, $nt ) { WatchedItem::doDuplicateEntries( $ot->getSubjectPage(), $nt->getSubjectPage() ); WatchedItem::doDuplicateEntries( $ot->getTalkPage(), $nt->getTalkPage() ); } - /** - * @static - * @access private - */ - function doDuplicateEntries( $ot, $nt ) { + private static function doDuplicateEntries( $ot, $nt ) { $fname = "WatchedItem::duplicateEntries"; $oldnamespace = $ot->getNamespace(); $newnamespace = $nt->getNamespace(); @@ -165,4 +160,4 @@ class WatchedItem { } -?> + diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php new file mode 100644 index 00000000..e03225a3 --- /dev/null +++ b/includes/WatchlistEditor.php @@ -0,0 +1,493 @@ + + */ +class WatchlistEditor { + + /** + * Editing modes + */ + const EDIT_CLEAR = 1; + const EDIT_RAW = 2; + const EDIT_NORMAL = 3; + + /** + * Main execution point + * + * @param User $user + * @param OutputPage $output + * @param WebRequest $request + * @param int $mode + */ + public function execute( $user, $output, $request, $mode ) { + global $wgUser; + if( wfReadOnly() ) { + $output->readOnlyPage(); + return; + } + switch( $mode ) { + case self::EDIT_CLEAR: + $output->setPageTitle( wfMsg( 'watchlistedit-clear-title' ) ); + if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) { + $this->clearWatchlist( $user ); + $user->invalidateCache(); + $output->addHtml( wfMsgExt( 'watchlistedit-clear-done', 'parse' ) ); + } else { + $this->showClearForm( $output, $user ); + } + break; + case self::EDIT_RAW: + $output->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) ); + if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) { + $wanted = $this->extractTitles( $request->getText( 'titles' ) ); + $current = $this->getWatchlist( $user ); + if( count( $wanted ) > 0 ) { + $toWatch = array_diff( $wanted, $current ); + $toUnwatch = array_diff( $current, $wanted ); + $this->watchTitles( $toWatch, $user ); + $this->unwatchTitles( $toUnwatch, $user ); + $user->invalidateCache(); + if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) + $output->addHtml( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) ); + if( ( $count = count( $toWatch ) ) > 0 ) { + $output->addHtml( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) ); + $this->showTitles( $toWatch, $output, $wgUser->getSkin() ); + } + if( ( $count = count( $toUnwatch ) ) > 0 ) { + $output->addHtml( wfMsgExt( 'watchlistedit-raw-removed', 'parse', $count ) ); + $this->showTitles( $toUnwatch, $output, $wgUser->getSkin() ); + } + } else { + $this->clearWatchlist( $user ); + $user->invalidateCache(); + $output->addHtml( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) ); + $this->showTitles( $current, $output, $wgUser->getSkin() ); + } + } + $this->showRawForm( $output, $user ); + break; + case self::EDIT_NORMAL: + $output->setPageTitle( wfMsg( 'watchlistedit-normal-title' ) ); + if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) { + $titles = $this->extractTitles( $request->getArray( 'titles' ) ); + $this->unwatchTitles( $titles, $user ); + $user->invalidateCache(); + $output->addHtml( wfMsgExt( 'watchlistedit-normal-done', 'parse', + $GLOBALS['wgLang']->formatNum( count( $titles ) ) ) ); + $this->showTitles( $titles, $output, $wgUser->getSkin() ); + } + $this->showNormalForm( $output, $user ); + } + } + + /** + * Check the edit token from a form submission + * + * @param WebRequest $request + * @param User $user + * @return bool + */ + private function checkToken( $request, $user ) { + return $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' ); + } + + /** + * Extract a list of titles from a blob of text, returning + * (prefixed) strings; unwatchable titles are ignored + * + * @param mixed $list + * @return array + */ + private function extractTitles( $list ) { + $titles = array(); + if( !is_array( $list ) ) { + $list = explode( "\n", trim( $list ) ); + if( !is_array( $list ) ) + return array(); + } + foreach( $list as $text ) { + $text = trim( $text ); + if( strlen( $text ) > 0 ) { + $title = Title::newFromText( $text ); + if( $title instanceof Title && $title->isWatchable() ) + $titles[] = $title->getPrefixedText(); + } + } + return array_unique( $titles ); + } + + /** + * Print out a list of linked titles + * + * $titles can be an array of strings or Title objects; the former + * is preferred, since Titles are very memory-heavy + * + * @param array $titles An array of strings, or Title objects + * @param OutputPage $output + * @param Skin $skin + */ + private function showTitles( $titles, $output, $skin ) { + $talk = wfMsgHtml( 'talkpagelinktext' ); + // Do a batch existence check + $batch = new LinkBatch(); + foreach( $titles as $title ) { + if( !$title instanceof Title ) + $title = Title::newFromText( $title ); + if( $title instanceof Title ) { + $batch->addObj( $title ); + $batch->addObj( $title->getTalkPage() ); + } + } + $batch->execute(); + // Print out the list + $output->addHtml( "
      \n" ); + foreach( $titles as $title ) { + if( !$title instanceof Title ) + $title = Title::newFromText( $title ); + if( $title instanceof Title ) { + $output->addHtml( "
    • " . $skin->makeLinkObj( $title ) + . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")
    • \n" ); + } + } + $output->addHtml( "
    \n" ); + } + + /** + * Count the number of titles on a user's watchlist, excluding talk pages + * + * @param User $user + * @return int + */ + private function countWatchlist( $user ) { + $dbr = wfGetDB( DB_MASTER ); + $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->getId() ), __METHOD__ ); + $row = $dbr->fetchObject( $res ); + return ceil( $row->count / 2 ); // Paranoia + } + + /** + * Prepare a list of titles on a user's watchlist (excluding talk pages) + * and return an array of (prefixed) strings + * + * @param User $user + * @return array + */ + private function getWatchlist( $user ) { + $list = array(); + $dbr = wfGetDB( DB_MASTER ); + $res = $dbr->select( + 'watchlist', + '*', + array( + 'wl_user' => $user->getId(), + ), + __METHOD__ + ); + if( $res->numRows() > 0 ) { + while( $row = $res->fetchObject() ) { + $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ); + if( $title instanceof Title && !$title->isTalkPage() ) + $list[] = $title->getPrefixedText(); + } + $res->free(); + } + return $list; + } + + /** + * Get a list of titles on a user's watchlist, excluding talk pages, + * and return as a two-dimensional array with namespace, title and + * redirect status + * + * @param User $user + * @return array + */ + private function getWatchlistInfo( $user ) { + $titles = array(); + $dbr = wfGetDB( DB_MASTER ); + $uid = intval( $user->getId() ); + list( $watchlist, $page ) = $dbr->tableNamesN( 'watchlist', 'page' ); + $sql = "SELECT wl_namespace, wl_title, page_id, page_is_redirect + FROM {$watchlist} LEFT JOIN {$page} ON ( wl_namespace = page_namespace + AND wl_title = page_title ) WHERE wl_user = {$uid}"; + $res = $dbr->query( $sql, __METHOD__ ); + if( $res && $dbr->numRows( $res ) > 0 ) { + $cache = LinkCache::singleton(); + while( $row = $dbr->fetchObject( $res ) ) { + $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title ); + if( $title instanceof Title ) { + // Update the link cache while we're at it + if( $row->page_id ) { + $cache->addGoodLinkObj( $row->page_id, $title ); + } else { + $cache->addBadLinkObj( $title ); + } + // Ignore non-talk + if( !$title->isTalkPage() ) + $titles[$row->wl_namespace][$row->wl_title] = $row->page_is_redirect; + } + } + } + return $titles; + } + + /** + * Show a message indicating the number of items on the user's watchlist, + * and return this count for additional checking + * + * @param OutputPage $output + * @param User $user + * @return int + */ + private function showItemCount( $output, $user ) { + if( ( $count = $this->countWatchlist( $user ) ) > 0 ) { + $output->addHtml( wfMsgExt( 'watchlistedit-numitems', 'parse', + $GLOBALS['wgLang']->formatNum( $count ) ) ); + } else { + $output->addHtml( wfMsgExt( 'watchlistedit-noitems', 'parse' ) ); + } + return $count; + } + + /** + * Remove all titles from a user's watchlist + * + * @param User $user + */ + private function clearWatchlist( $user ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'watchlist', array( 'wl_user' => $user->getId() ), __METHOD__ ); + } + + /** + * Add a list of titles to a user's watchlist + * + * $titles can be an array of strings or Title objects; the former + * is preferred, since Titles are very memory-heavy + * + * @param array $titles An array of strings, or Title objects + * @param User $user + */ + private function watchTitles( $titles, $user ) { + $dbw = wfGetDB( DB_MASTER ); + $rows = array(); + foreach( $titles as $title ) { + if( !$title instanceof Title ) + $title = Title::newFromText( $title ); + if( $title instanceof Title ) { + $rows[] = array( + 'wl_user' => $user->getId(), + 'wl_namespace' => ( $title->getNamespace() & ~1 ), + 'wl_title' => $title->getDBkey(), + 'wl_notificationtimestamp' => null, + ); + $rows[] = array( + 'wl_user' => $user->getId(), + 'wl_namespace' => ( $title->getNamespace() | 1 ), + 'wl_title' => $title->getDBkey(), + 'wl_notificationtimestamp' => null, + ); + } + } + $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' ); + } + + /** + * Remove a list of titles from a user's watchlist + * + * $titles can be an array of strings or Title objects; the former + * is preferred, since Titles are very memory-heavy + * + * @param array $titles An array of strings, or Title objects + * @param User $user + */ + private function unwatchTitles( $titles, $user ) { + $dbw = wfGetDB( DB_MASTER ); + foreach( $titles as $title ) { + if( !$title instanceof Title ) + $title = Title::newFromText( $title ); + if( $title instanceof Title ) { + $dbw->delete( + 'watchlist', + array( + 'wl_user' => $user->getId(), + 'wl_namespace' => ( $title->getNamespace() & ~1 ), + 'wl_title' => $title->getDBkey(), + ), + __METHOD__ + ); + $dbw->delete( + 'watchlist', + array( + 'wl_user' => $user->getId(), + 'wl_namespace' => ( $title->getNamespace() | 1 ), + 'wl_title' => $title->getDBkey(), + ), + __METHOD__ + ); + } + } + } + + /** + * Show a confirmation form for users wishing to clear their watchlist + * + * @param OutputPage $output + * @param User $user + */ + private function showClearForm( $output, $user ) { + global $wgUser; + if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) { + $self = SpecialPage::getTitleFor( 'Watchlist' ); + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $self->getLocalUrl( 'action=clear' ) ) ); + $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) ); + $form .= '
    ' . wfMsgHtml( 'watchlistedit-clear-legend' ) . ''; + $form .= wfMsgExt( 'watchlistedit-clear-confirm', 'parse' ); + $form .= '

    ' . Xml::submitButton( wfMsg( 'watchlistedit-clear-submit' ) ) . '

    '; + $form .= '
    '; + $output->addHtml( $form ); + } + } + + /** + * Show the standard watchlist editing form + * + * @param OutputPage $output + * @param User $user + */ + private function showNormalForm( $output, $user ) { + global $wgUser; + if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) { + $self = SpecialPage::getTitleFor( 'Watchlist' ); + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $self->getLocalUrl( 'action=edit' ) ) ); + $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) ); + $form .= '
    ' . wfMsgHtml( 'watchlistedit-normal-legend' ) . ''; + $form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' ); + foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) { + $form .= '

    ' . $this->getNamespaceHeading( $namespace ) . '

    '; + $form .= '
      '; + foreach( $pages as $dbkey => $redirect ) { + $title = Title::makeTitleSafe( $namespace, $dbkey ); + $form .= $this->buildRemoveLine( $title, $redirect, $wgUser->getSkin() ); + } + $form .= '
    '; + } + $form .= '

    ' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '

    '; + $form .= '
    '; + $output->addHtml( $form ); + } + } + + /** + * Get the correct "heading" for a namespace + * + * @param int $namespace + * @return string + */ + private function getNamespaceHeading( $namespace ) { + return $namespace == NS_MAIN + ? wfMsgHtml( 'blanknamespace' ) + : htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) ); + } + + /** + * Build a single list item containing a check box selecting a title + * and a link to that title, with various additional bits + * + * @param Title $title + * @param bool $redirect + * @param Skin $skin + * @return string + */ + private function buildRemoveLine( $title, $redirect, $skin ) { + $link = $skin->makeLinkObj( $title ); + if( $redirect ) + $link = '' . $link . ''; + $tools[] = $skin->makeLinkObj( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) ); + if( $title->exists() ) { + $tools[] = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'history_short' ), 'action=history' ); + } + if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) { + $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) ); + } + return '
  • ' + . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) ) + . $link . ' (' . implode( ' | ', $tools ) . ')' . '
  • '; + } + + /** + * Show a form for editing the watchlist in "raw" mode + * + * @param OutputPage $output + * @param User $user + */ + public function showRawForm( $output, $user ) { + global $wgUser; + $this->showItemCount( $output, $user ); + $self = SpecialPage::getTitleFor( 'Watchlist' ); + $form = Xml::openElement( 'form', array( 'method' => 'post', + 'action' => $self->getLocalUrl( 'action=raw' ) ) ); + $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) ); + $form .= '
    ' . wfMsgHtml( 'watchlistedit-raw-legend' ) . ''; + $form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' ); + $form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' ); + $form .= "
    \n"; + $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles', + 'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) ); + $titles = $this->getWatchlist( $user ); + foreach( $titles as $title ) + $form .= htmlspecialchars( $title ) . "\n"; + $form .= ''; + $form .= '

    ' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '

    '; + $form .= '
    '; + $output->addHtml( $form ); + } + + /** + * Determine whether we are editing the watchlist, and if so, what + * kind of editing operation + * + * @param WebRequest $request + * @param mixed $par + * @return int + */ + public static function getMode( $request, $par ) { + $mode = strtolower( $request->getVal( 'action', $par ) ); + switch( $mode ) { + case 'clear': + return self::EDIT_CLEAR; + case 'raw': + return self::EDIT_RAW; + case 'edit': + return self::EDIT_NORMAL; + default: + return false; + } + } + + /** + * Build a set of links for convenient navigation + * between watchlist viewing and editing modes + * + * @param Skin $skin Skin to use + * @return string + */ + public static function buildTools( $skin ) { + $tools = array(); + $self = SpecialPage::getTitleFor( 'Watchlist' ); + $modes = array( 'view' => '', 'edit' => 'edit', 'raw' => 'raw', 'clear' => 'clear' ); + foreach( $modes as $mode => $action ) { + $action = $action ? "action={$action}" : ''; + $tools[] = $skin->makeKnownLinkObj( $self, wfMsgHtml( "watchlisttools-{$mode}" ), $action ); + } + return implode( ' | ', $tools ); + } + +} diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 53273a22..aa9885f0 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -44,17 +44,93 @@ if ( !function_exists( '__autoload' ) ) { class WebRequest { function __construct() { $this->checkMagicQuotes(); + } + + /** + * Check for title, action, and/or variant data in the URL + * and interpolate it into the GET variables. + * This should only be run after $wgContLang is available, + * as we may need the list of language variants to determine + * available variant URLs. + */ + function interpolateTitle() { global $wgUsePathInfo; if ( $wgUsePathInfo ) { - if ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) { - # Mangled PATH_INFO - # http://bugs.php.net/bug.php?id=31892 - # Also reported when ini_get('cgi.fix_pathinfo')==false - $_GET['title'] = $_REQUEST['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); - } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') && $wgUsePathInfo ) { - $_GET['title'] = $_REQUEST['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + // PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892 + // And also by Apache 2.x, double slashes are converted to single slashes. + // So we will use REQUEST_URI if possible. + $matches = array(); + if ( !empty( $_SERVER['REQUEST_URI'] ) ) { + // Slurp out the path portion to examine... + $url = $_SERVER['REQUEST_URI']; + if ( !preg_match( '!^https?://!', $url ) ) { + $url = 'http://unused' . $url; + } + $a = parse_url( $url ); + if( $a ) { + $path = $a['path']; + + global $wgArticlePath; + $matches = $this->extractTitle( $path, $wgArticlePath ); + + global $wgActionPaths; + if( !$matches && $wgActionPaths) { + $matches = $this->extractTitle( $path, $wgActionPaths, 'action' ); + } + + global $wgVariantArticlePath, $wgContLang; + if( !$matches && $wgVariantArticlePath ) { + $variantPaths = array(); + foreach( $wgContLang->getVariants() as $variant ) { + $variantPaths[$variant] = + str_replace( '$2', $variant, $wgVariantArticlePath ); + } + $matches = $this->extractTitle( $path, $variantPaths, 'variant' ); + } + } + } elseif ( isset( $_SERVER['ORIG_PATH_INFO'] ) && $_SERVER['ORIG_PATH_INFO'] != '' ) { + // Mangled PATH_INFO + // http://bugs.php.net/bug.php?id=31892 + // Also reported when ini_get('cgi.fix_pathinfo')==false + $matches['title'] = substr( $_SERVER['ORIG_PATH_INFO'], 1 ); + + } elseif ( isset( $_SERVER['PATH_INFO'] ) && ($_SERVER['PATH_INFO'] != '') ) { + // Regular old PATH_INFO yay + $matches['title'] = substr( $_SERVER['PATH_INFO'], 1 ); + } + foreach( $matches as $key => $val) { + $_GET[$key] = $_REQUEST[$key] = $val; + } + } + } + + /** + * Internal URL rewriting function; tries to extract page title and, + * optionally, one other fixed parameter value from a URL path. + * + * @param string $path the URL path given from the client + * @param array $bases one or more URLs, optionally with $1 at the end + * @param string $key if provided, the matching key in $bases will be + * passed on as the value of this URL parameter + * @return array of URL variables to interpolate; empty if no match + */ + private function extractTitle( $path, $bases, $key=false ) { + foreach( (array)$bases as $keyValue => $base ) { + // Find the part after $wgArticlePath + $base = str_replace( '$1', '', $base ); + $baseLen = strlen( $base ); + if( substr( $path, 0, $baseLen ) == $base ) { + $raw = substr( $path, $baseLen ); + if( $raw !== '' ) { + $matches = array( 'title' => rawurldecode( $raw ) ); + if( $key ) { + $matches[$key] = $keyValue; + } + return $matches; + } } } + return array(); } private $_response; @@ -543,4 +619,4 @@ class FauxRequest extends WebRequest { } -?> + diff --git a/includes/WebResponse.php b/includes/WebResponse.php index 92343195..f1578885 100644 --- a/includes/WebResponse.php +++ b/includes/WebResponse.php @@ -17,4 +17,4 @@ class WebResponse { } } -?> + diff --git a/includes/WebStart.php b/includes/WebStart.php index 55c96488..a9a6ad5f 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -101,4 +101,4 @@ wfProfileOut( 'WebStart.php-ob_start' ); if ( !defined( 'MW_NO_SETUP' ) ) { require_once( './includes/Setup.php' ); } -?> + diff --git a/includes/Wiki.php b/includes/Wiki.php index 612e58ee..72a6a61d 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -57,14 +57,18 @@ class MediaWiki { } function checkMaxLag( $maxLag ) { - global $wgLoadBalancer; + global $wgLoadBalancer, $wgShowHostnames; list( $host, $lag ) = $wgLoadBalancer->getMaxLag(); if ( $lag > $maxLag ) { 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' ); - echo "Waiting for $host: $lag seconds lagged\n"; + if( $wgShowHostnames ) { + echo "Waiting for $host: $lag seconds lagged\n"; + } else { + echo "Waiting for a database server: $lag seconds lagged\n"; + } return false; } else { return true; @@ -98,6 +102,14 @@ class MediaWiki { $lang->findVariantLink( $title, $ret ); } + if ( ( $oldid = $request->getInt( 'oldid' ) ) + && ( is_null( $ret ) || $ret->getNamespace() != NS_SPECIAL ) ) { + // Allow oldid to override a changed or missing title. + $rev = Revision::newFromId( $oldid ); + if( $rev ) { + $ret = $rev->getTitle(); + } + } return $ret ; } @@ -106,16 +118,14 @@ class MediaWiki { */ function preliminaryChecks ( &$title, &$output, $request ) { - # Debug statement for user levels - // print_r($wgUser); - - $search = $request->getText( 'search' ); - if( !is_null( $search ) && $search !== '' ) { + if( $request->getCheck( 'search' ) ) { // Compatibility with old search URLs which didn't use Special:Search + // Just check for presence here, so blank requests still + // show the search page when using ugly URLs (bug 8054). + // Do this above the read whitelist check for security... $title = SpecialPage::getTitleFor( 'Search' ); } - $this->setVal( 'Search', $search ); # If the user is not logged in, the Namespace:title of the article must be in # the Read array in order for the user to see it. (We have to check here to @@ -135,13 +145,8 @@ class MediaWiki { global $wgRequest; wfProfileIn( 'MediaWiki::initializeSpecialCases' ); - $search = $this->getVal('Search'); $action = $this->getVal('Action'); - if( !$this->getVal('DisableInternalSearch') && !is_null( $search ) && $search !== '' ) { - require_once( 'includes/SpecialSearch.php' ); - $title = SpecialPage::getTitleFor( 'Search' ); - wfSpecialSearch(); - } else if( !$title or $title->getDBkey() == '' ) { + if( !$title or $title->getDBkey() == '' ) { $title = SpecialPage::getTitleFor( 'Badtitle' ); # Die now before we mess up $wgArticle and the skin stops working throw new ErrorPageError( 'badtitle', 'badtitletext' ); @@ -158,7 +163,7 @@ class MediaWiki { $title = SpecialPage::getTitleFor( 'Badtitle' ); throw new ErrorPageError( 'badtitle', 'badtitletext' ); } - } else if ( ( $action == 'view' ) && + } else if ( ( $action == 'view' ) && !$wgRequest->wasPosted() && (!isset( $this->GET['title'] ) || $title->getPrefixedDBKey() != $this->GET['title'] ) && !count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) ) { @@ -209,7 +214,7 @@ class MediaWiki { * @param Title $title * @return Article */ - function articleFromTitle( $title ) { + static function articleFromTitle( $title ) { $article = null; wfRunHooks('ArticleFromTitle', array( &$title, &$article ) ); if ( $article ) { @@ -460,4 +465,4 @@ class MediaWiki { }; /* End of class MediaWiki */ -?> + diff --git a/includes/WikiError.php b/includes/WikiError.php index 064db61a..efb645bb 100644 --- a/includes/WikiError.php +++ b/includes/WikiError.php @@ -121,4 +121,4 @@ class WikiXmlError extends WikiError { } } -?> + diff --git a/includes/Xml.php b/includes/Xml.php index 0fedcfa0..fe4bb0cd 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -19,9 +19,7 @@ class Xml { public static function element( $element, $attribs = null, $contents = '') { $out = '<' . $element; if( !is_null( $attribs ) ) { - foreach( $attribs as $name => $val ) { - $out .= ' ' . $name . '="' . Sanitizer::encodeAttribute( $val ) . '"'; - } + $out .= self::expandAttributes( $attribs ); } if( is_null( $contents ) ) { $out .= '>'; @@ -35,6 +33,26 @@ class Xml { return $out; } + /** + * Given an array of ('attributename' => 'value'), it generates the code + * to set the XML attributes : attributename="value". + * The values are passed to Sanitizer::encodeAttribute. + * Return null if no attributes given. + * @param $attribs Array of attributes for an XML element + */ + private static function expandAttributes( $attribs ) { + $out = ''; + if( is_null( $attribs ) ) { + return null; + } elseif( is_array( $attribs ) ) { + foreach( $attribs as $name => $val ) + $out .= " {$name}=\"" . Sanitizer::encodeAttribute( $val ) . '"'; + return $out; + } else { + throw new MWException( 'Expected attribute array, got something else in ' . __METHOD__ ); + } + } + /** * Format an XML element as with self::element(), but run text through the * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8 @@ -57,8 +75,12 @@ class Xml { return self::element( $element, $attribs, $contents ); } - // Shortcuts - public static function openElement( $element, $attribs = null ) { return self::element( $element, $attribs, null ); } + /** This open an XML element */ + public static function openElement( $element, $attribs = null ) { + return '<' . $element . self::expandAttributes( $attribs ) . '>'; + } + + // Shortcut public static function closeElement( $element ) { return ""; } /** @@ -66,48 +88,99 @@ class Xml { * content you have is already valid xml. */ public static function tags( $element, $attribs = null, $contents ) { - return self::element( $element, $attribs, null ) . $contents . ""; + return self::openElement( $element, $attribs ) . $contents . ""; } /** - * Create a namespace selector + * Build a drop-down box for selecting a namespace * - * @param $selected Mixed: the namespace which should be selected, default '' - * @param $allnamespaces String: value of a special item denoting all namespaces. Null to not include (default) - * @param $includehidden Bool: include hidden namespaces? - * @return String: Html string containing the namespace selector + * @param mixed $selected Namespace which should be pre-selected + * @param mixed $all Value of an item denoting all namespaces, or null to omit + * @param bool $hidden Include hidden namespaces? [WTF? --RC] + * @return string */ - public static function namespaceSelector($selected = '', $allnamespaces = null, $includehidden=false) { + public static function namespaceSelector( $selected = '', $all = null, $hidden = false, $element_name = 'namespace' ) { global $wgContLang; - if( $selected !== '' ) { - if( is_null( $selected ) ) { - // No namespace selected; let exact match work without hitting Main - $selected = ''; - } else { - // Let input be numeric strings without breaking the empty match. - $selected = intval( $selected ); - } + $namespaces = $wgContLang->getFormattedNamespaces(); + $options = array(); + + if( !is_null( $all ) ) + $namespaces = array( $all => wfMsg( 'namespacesall' ) ) + $namespaces; + foreach( $namespaces as $index => $name ) { + if( $index < NS_MAIN ) + continue; + if( $index === 0 ) + $name = wfMsg( 'blanknamespace' ); + $options[] = self::option( $name, $index, $index === $selected ); } - $s = "\n\n"; - return $s; + + return array( + Xml::label( wfMsg('yourlanguage'), 'wpUserLanguage' ), + Xml::tags( 'select', + array( 'id' => 'wpUserLanguage', 'name' => 'wpUserLanguage' ), + $options + ) + ); + } public static function span( $text, $class, $attribs=array() ) { @@ -125,6 +198,14 @@ class Xml { 'value' => $value ) + $attribs ); } + /** + * Convenience function to build an HTML password input field + * @return string HTML + */ + public static function password( $name, $size=false, $value=false, $attribs=array() ) { + return self::input( $name, $size, $value, array_merge($attribs, array('type' => 'password'))); + } + /** * Internal function for use in checkboxes and radio buttons and such. * @return array @@ -263,13 +344,22 @@ class Xml { # To avoid any complaints about bad entity refs "&" => "\\x26", + + # Work around https://bugzilla.mozilla.org/show_bug.cgi?id=274152 + # Encode certain Unicode formatting chars so affected + # versions of Gecko don't misinterpret our strings; + # this is a common problem with Farsi text. + "\xe2\x80\x8c" => "\\u200c", // ZERO WIDTH NON-JOINER + "\xe2\x80\x8d" => "\\u200d", // ZERO WIDTH JOINER ); return strtr( $string, $pairs ); } /** * Encode a variable of unknown type to JavaScript. - * Doesn't support hashtables just yet. + * Arrays are converted to JS arrays, objects are converted to JS associative + * arrays (objects). So cast your PHP associative arrays to objects before + * passing them to here. */ public static function encodeJsVar( $value ) { if ( is_bool( $value ) ) { @@ -280,13 +370,23 @@ class Xml { $s = $value; } elseif ( is_array( $value ) ) { $s = '['; - foreach ( $value as $name => $elt ) { + foreach ( $value as $elt ) { if ( $s != '[' ) { $s .= ', '; } $s .= self::encodeJsVar( $elt ); } $s .= ']'; + } elseif ( is_object( $value ) ) { + $s = '{'; + foreach ( (array)$value as $name => $elt ) { + if ( $s != '{' ) { + $s .= ', '; + } + $s .= '"' . self::escapeJsString( $name ) . '": ' . + self::encodeJsVar( $elt ); + } + $s .= '}'; } else { $s = '"' . self::escapeJsString( $value ) . '"'; } @@ -352,4 +452,4 @@ class Xml { $in ); } } -?> + diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php index 326c4953..2e86aa7d 100644 --- a/includes/XmlFunctions.php +++ b/includes/XmlFunctions.php @@ -59,7 +59,4 @@ function wfIsWellFormedXml( $text ) { } function wfIsWellFormedXmlFragment( $text ) { return Xml::isWellFormedXmlFragment( $text ); -} - - -?> +} \ No newline at end of file diff --git a/includes/ZhClient.php b/includes/ZhClient.php index fe965f65..7f2c5cbf 100644 --- a/includes/ZhClient.php +++ b/includes/ZhClient.php @@ -144,4 +144,3 @@ class ZhClient { fclose($this->mFP); } } -?> \ No newline at end of file diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php index 1ccd6a7e..1b9d884b 100644 --- a/includes/ZhConversion.php +++ b/includes/ZhConversion.php @@ -8453,4 +8453,3 @@ $zh2SG=array( "房价" => "屋价", "泡麵" => "快速面", ); -?> \ No newline at end of file diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index c4218825..b324c52f 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,7 +24,16 @@ */ /** - * @todo Document - e.g. Provide top-level description of this class. + * This abstract class implements many basic API functions, and is the base of all API classes. + * The class functions are divided into several areas of functionality: + * + * Module parameters: Derived classes can define getAllowedParams() to specify which parameters to expect, + * how to parse and validate them. + * + * Profiling: various methods to allow keeping tabs on various tasks and their time costs + * + * Self-documentation: code to allow api to document its own state. + * * @addtogroup API */ abstract class ApiBase { @@ -34,24 +43,24 @@ abstract class ApiBase { const PARAM_DFLT = 0; const PARAM_ISMULTI = 1; const PARAM_TYPE = 2; - const PARAM_MAX1 = 3; + const PARAM_MAX = 3; const PARAM_MAX2 = 4; const PARAM_MIN = 5; - const LIMIT_BIG1 = 500; // Fast query, user's limit - const LIMIT_BIG2 = 5000; // Fast query, bot's limit - const LIMIT_SML1 = 50; // Slow query, user's limit - const LIMIT_SML2 = 500; // Slow query, bot's limit + const LIMIT_BIG1 = 500; // Fast query, std user limit + const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit + const LIMIT_SML1 = 50; // Slow query, std user limit + const LIMIT_SML2 = 500; // Slow query, bot/sysop limit - private $mMainModule, $mModuleName, $mParamPrefix; + private $mMainModule, $mModuleName, $mModulePrefix; /** * Constructor */ - public function __construct($mainModule, $moduleName, $paramPrefix = '') { + public function __construct($mainModule, $moduleName, $modulePrefix = '') { $this->mMainModule = $mainModule; $this->mModuleName = $moduleName; - $this->mParamPrefix = $paramPrefix; + $this->mModulePrefix = $modulePrefix; } /** @@ -66,6 +75,13 @@ abstract class ApiBase { return $this->mModuleName; } + /** + * Get parameter prefix (usually two letters or an empty string). + */ + public function getModulePrefix() { + return $this->mModulePrefix; + } + /** * Get the name of the module as shown in the profiler log */ @@ -108,6 +124,15 @@ abstract class ApiBase { return $this->getResult()->getData(); } + /** + * Set warning section for this module. Users should monitor this section to notice any changes in API. + */ + public function setWarning($warning) { + $msg = array(); + ApiResult :: setContent($msg, $warning); + $this->getResult()->addValue('warnings', $this->getModuleName(), $msg); + } + /** * If the module may only be used with a certain format module, * it should override this method to return an instance of that formatter. @@ -191,11 +216,38 @@ abstract class ApiBase { $prompt = 'One value: '; if (is_array($type)) { - $desc .= $paramPrefix . $prompt . implode(', ', $type); - } - elseif ($type == 'namespace') { - // Special handling because namespaces are type-limited, yet they are not given - $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces()); + $choices = array(); + $nothingPrompt = false; + foreach ($type as $t) + if ($t=='') + $nothingPrompt = 'Can be empty, or '; + else + $choices[] = $t; + $desc .= $paramPrefix . $nothingPrompt . $prompt . implode(', ', $choices); + } else { + switch ($type) { + case 'namespace': + // Special handling because namespaces are type-limited, yet they are not given + $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces()); + break; + case 'limit': + $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]} ({$paramSettings[self :: PARAM_MAX2]} for bots) allowed."; + break; + case 'integer': + $hasMin = isset($paramSettings[self :: PARAM_MIN]); + $hasMax = isset($paramSettings[self :: PARAM_MAX]); + if ($hasMin || $hasMax) { + if (!$hasMax) + $intRangeStr = "The value must be no less than {$paramSettings[self :: PARAM_MIN]}"; + elseif (!$hasMin) + $intRangeStr = "The value must be no more than {$paramSettings[self :: PARAM_MAX]}"; + else + $intRangeStr = "The value must be between {$paramSettings[self :: PARAM_MIN]} and {$paramSettings[self :: PARAM_MAX]}"; + + $desc .= $paramPrefix . $intRangeStr; + } + break; + } } } @@ -244,7 +296,7 @@ abstract class ApiBase { * Override this method to change parameter name during runtime */ public function encodeParamName($paramName) { - return $this->mParamPrefix . $paramName; + return $this->mModulePrefix . $paramName; } /** @@ -293,7 +345,7 @@ abstract class ApiBase { protected function getParameterFromSettings($paramName, $paramSettings) { // Some classes may decide to change parameter names - $paramName = $this->encodeParamName($paramName); + $encParamName = $this->encodeParamName($paramName); if (!is_array($paramSettings)) { $default = $paramSettings; @@ -316,19 +368,19 @@ abstract class ApiBase { if ($type == 'boolean') { if (isset ($default) && $default !== false) { // Having a default value of anything other than 'false' is pointless - ApiBase :: dieDebug(__METHOD__, "Boolean param $paramName's default is set to '$default'"); + ApiBase :: dieDebug(__METHOD__, "Boolean param $encParamName's default is set to '$default'"); } - $value = $this->getMain()->getRequest()->getCheck($paramName); + $value = $this->getMain()->getRequest()->getCheck($encParamName); } else { - $value = $this->getMain()->getRequest()->getVal($paramName, $default); + $value = $this->getMain()->getRequest()->getVal($encParamName, $default); if (isset ($value) && $type == 'namespace') $type = ApiBase :: getValidNamespaces(); } if (isset ($value) && ($multi || is_array($type))) - $value = $this->parseMultiValue($paramName, $value, $multi, is_array($type) ? $type : null); + $value = $this->parseMultiValue($encParamName, $value, $multi, is_array($type) ? $type : null); // More validation only when choices were not given // choices were validated in parseMultiValue() @@ -339,32 +391,48 @@ abstract class ApiBase { break; case 'string' : // nothing to do break; - case 'integer' : // Force everything using intval() + case 'integer' : // Force everything using intval() and optionally validate limits + $value = is_array($value) ? array_map('intval', $value) : intval($value); + $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : null; + $max = isset ($paramSettings[self :: PARAM_MAX]) ? $paramSettings[self :: PARAM_MAX] : null; + + if (!is_null($min) || !is_null($max)) { + $values = is_array($value) ? $value : array($value); + foreach ($values as $v) { + $this->validateLimit($paramName, $v, $min, $max); + } + } break; case 'limit' : - if (!isset ($paramSettings[self :: PARAM_MAX1]) || !isset ($paramSettings[self :: PARAM_MAX2])) - ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $paramName"); + if (!isset ($paramSettings[self :: PARAM_MAX]) || !isset ($paramSettings[self :: PARAM_MAX2])) + ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $encParamName"); if ($multi) - ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName"); $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0; $value = intval($value); - $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX1], $paramSettings[self :: PARAM_MAX2]); + $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]); break; case 'boolean' : if ($multi) - ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName"); break; case 'timestamp' : if ($multi) - ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName"); $value = wfTimestamp(TS_UNIX, $value); if ($value === 0) - $this->dieUsage("Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$paramName}"); + $this->dieUsage("Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}"); $value = wfTimestamp(TS_MW, $value); break; + case 'user' : + $title = Title::makeTitleSafe( NS_USER, $value ); + if ( is_null( $title ) ) + $this->dieUsage("Invalid value for user parameter $encParamName", "baduser_{$encParamName}"); + $value = $title->getText(); + break; default : - ApiBase :: dieDebug(__METHOD__, "Param $paramName's type is unknown - $type"); + ApiBase :: dieDebug(__METHOD__, "Param $encParamName's type is unknown - $type"); } } @@ -405,19 +473,26 @@ abstract class ApiBase { /** * Validate the value against the minimum and user/bot maximum limits. Prints usage info on failure. */ - function validateLimit($varname, $value, $min, $max, $botMax) { - if ($value < $min) { - $this->dieUsage("$varname may not be less than $min (set to $value)", $varname); + function validateLimit($paramName, $value, $min, $max, $botMax = null) { + if (!is_null($min) && $value < $min) { + $this->dieUsage($this->encodeParamName($paramName) . " may not be less than $min (set to $value)", $paramName); } - if ($this->getMain()->isBot()) { - if ($value > $botMax) { - $this->dieUsage("$varname may not be over $botMax (set to $value) for bots", $varname); + // Minimum is always validated, whereas maximum is checked only if not running in internal call mode + if ($this->getMain()->isInternalMode()) + return; + + // Optimization: do not check user's bot status unless really needed -- skips db query + // assumes $botMax >= $max + if (!is_null($max) && $value > $max) { + if (!is_null($botMax) && ($this->getMain()->isBot() || $this->getMain()->isSysop())) { + if ($value > $botMax) { + $this->dieUsage($this->encodeParamName($paramName) . " may not be over $botMax (set to $value) for bots or sysops", $paramName); + } + } else { + $this->dieUsage($this->encodeParamName($paramName) . " may not be over $max (set to $value) for users", $paramName); } } - elseif ($value > $max) { - $this->dieUsage("$varname may not be over $max (set to $value) for users", $varname); - } } /** @@ -526,11 +601,19 @@ abstract class ApiBase { ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first'); return $this->mDBTime; } + + public static function debugPrint($value, $name = 'unknown', $backtrace = false) { + print "\n\n
    Debuging value '$name':\n\n";
    +		var_export($value);
    +		if ($backtrace)
    +			print "\n" . wfBacktrace();
    +		print "\n
    \n"; + } public abstract function getVersion(); public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiBase.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiBase.php 24934 2007-08-20 08:04:12Z nickj $'; } } -?> + diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 7918ee0e..b2f6ceff 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,10 @@ if (!defined('MEDIAWIKI')) { } /** + * This action allows users to get their watchlist items in RSS/Atom formats. + * When executed, it performs a nested call to the API to get the needed data, + * and formats it in a proper format. + * * @addtogroup API */ class ApiFeedWatchlist extends ApiBase { @@ -37,47 +41,81 @@ class ApiFeedWatchlist extends ApiBase { parent :: __construct($main, $action); } + /** + * This module uses a custom feed wrapper printer. + */ public function getCustomPrinter() { return new ApiFormatFeedWrapper($this->getMain()); } + /** + * Make a nested call to the API to request watchlist items in the last $hours. + * Wrap the result as an RSS/Atom feed. + */ public function execute() { - $feedformat = null; - extract($this->extractRequestParams()); - - // limit to 1 day - $startTime = wfTimestamp(TS_MW, time() - intval(1 * 86400)); - - // Prepare nested request - $params = new FauxRequest(array ( - 'action' => 'query', - 'meta' => 'siteinfo', - 'siprop' => 'general', - 'list' => 'watchlist', - 'wlprop' => 'user|comment|timestamp', - 'wlstart' => $startTime, - 'wllimit' => 50 - )); - - // Execute - $module = new ApiMain($params); - $module->execute(); - - // Get data array - $data = $module->getResultData(); - - $feedItems = array (); - foreach ($data['query']['watchlist'] as $info) { - $feedItems[] = $this->createFeedItem($info); - } - + global $wgFeedClasses, $wgSitename, $wgContLanguageCode; - $feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']'; - $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl(); - - $feed = new $wgFeedClasses[$feedformat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl); - ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems); + try { + $params = $this->extractRequestParams(); + + // limit to the number of hours going from now back + $endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60)); + + // Prepare nested request + $fauxReq = new FauxRequest(array ( + 'action' => 'query', + 'meta' => 'siteinfo', + 'siprop' => 'general', + 'list' => 'watchlist', + 'wlprop' => 'title|user|comment|timestamp', + 'wldir' => 'older', // reverse order - from newest to oldest + 'wlend' => $endTime, // stop at this time + 'wllimit' => 50 + )); + + // Execute + $module = new ApiMain($fauxReq); + $module->execute(); + + // Get data array + $data = $module->getResultData(); + + $feedItems = array (); + foreach ($data['query']['watchlist'] as $info) { + $feedItems[] = $this->createFeedItem($info); + } + + $feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']'; + $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl(); + + $feed = new $wgFeedClasses[$params['feedformat']] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl); + + ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems); + + } catch (Exception $e) { + + // Error results should not be cached + $this->getMain()->setCacheMaxAge(0); + + $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']'; + $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl(); + + $feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss'; + $feed = new $wgFeedClasses[$feedFormat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl); + + + if ($e instanceof UsageException) { + $errorCode = $e->getCodeString(); + } else { + // Something is seriously wrong + $errorCode = 'internal_api_error'; + } + + $errorText = $e->getMessage(); + $feedItems[] = new FeedItem("Error ($errorCode)", $errorText, "", "", ""); + ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems); + } } private function createFeedItem($info) { @@ -100,13 +138,20 @@ class ApiFeedWatchlist extends ApiBase { 'feedformat' => array ( ApiBase :: PARAM_DFLT => 'rss', ApiBase :: PARAM_TYPE => $feedFormatNames + ), + 'hours' => array ( + ApiBase :: PARAM_DFLT => 24, + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => 72, ) ); } protected function getParamDescription() { return array ( - 'feedformat' => 'The format of the feed' + 'feedformat' => 'The format of the feed', + 'hours' => 'List pages modified within this many hours from now' ); } @@ -121,7 +166,7 @@ class ApiFeedWatchlist extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFeedWatchlist.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFeedWatchlist.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 782a4161..861310d2 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * This is the abstract base class for API formatters. + * * @addtogroup API */ abstract class ApiFormatBase extends ApiBase { @@ -36,7 +38,8 @@ abstract class ApiFormatBase extends ApiBase { private $mIsHtml, $mFormat; /** - * Constructor + * Create a new instance of the formatter. + * If the format name ends with 'fm', wrap its output in the proper HTML. */ public function __construct($main, $format) { parent :: __construct($main, $format); @@ -56,6 +59,11 @@ abstract class ApiFormatBase extends ApiBase { */ public abstract function getMimeType(); + /** + * If formatter outputs data results as is, the results must first be sanitized. + * An XML formatter on the other hand uses special tags, such as "_element" for special handling, + * and thus needs to override this function to return true. + */ public function getNeedsRawData() { return false; } @@ -77,6 +85,7 @@ abstract class ApiFormatBase extends ApiBase { function initPrinter($isError) { $isHtml = $this->getIsHtml(); $mime = $isHtml ? 'text/html' : $this->getMimeType(); + $script = wfScript( 'api' ); // Some printers (ex. Feed) do their own header settings, // in which case $mime will be set to null @@ -96,14 +105,14 @@ abstract class ApiFormatBase extends ApiBase {
    -You are looking at the HTML representation of the mFormat?> format.
    +You are looking at the HTML representation of the mFormat ); ?> format.
    HTML is good for debugging, but probably is not suitable for your application.
    -Please see "format" parameter documentation at the API help -for more information. +See complete documentation, or +API help for more information.
    getIsHtml()) echo $this->formatHTML($text); @@ -152,9 +165,9 @@ for more information. $text = preg_replace('/\<(!--.*?--|.*?)\>/', '<\1>', $text); // identify URLs $protos = "http|https|ftp|gopher"; - $text = ereg_replace("($protos)://[^ '\"()<\n]+", '\\0', $text); + $text = ereg_replace("($protos)://[^ \\'\"()<\n]+", '\\0', $text); // identify requests to api.php - $text = ereg_replace("api\\.php\\?[^ ()<\n\t]+", '\\0', $text); + $text = ereg_replace("api\\.php\\?[^ \\()<\n\t]+", '\\0', $text); // make strings inside * bold $text = ereg_replace("\\*[^<>\n]+\\*", '\\0', $text); // make strings inside $ italic @@ -175,7 +188,7 @@ for more information. } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $'; } } @@ -190,7 +203,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } /** - * Call this method to initialize output data + * Call this method to initialize output data. See self::execute() */ public static function setResult($result, $feed, $feedItems) { // Store output in the Result data. @@ -214,6 +227,11 @@ class ApiFormatFeedWrapper extends ApiFormatBase { return true; } + /** + * This class expects the result data to be in a custom format set by self::setResult() + * $result['_feed'] - an instance of one of the $wgFeedClasses classes + * $result['_feeditems'] - an array of FeedItem instances + */ public function execute() { $data = $this->getResultData(); if (isset ($data['_feed']) && isset ($data['_feeditems'])) { @@ -232,7 +250,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 25746 2007-09-10 21:36:51Z brion $'; } } -?> diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index dd1847c4..ed9bd938 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -49,14 +49,35 @@ class ApiFormatJson extends ApiFormatBase { } public function execute() { + $prefix = $suffix = ""; + + $params = $this->extractRequestParams(); + $callback = $params['callback']; + if(!is_null($callback)) { + $prefix = ereg_replace("[^_A-Za-z0-9]", "", $callback ) . "("; + $suffix = ")"; + } + if (!function_exists('json_encode') || $this->getIsHtml()) { $json = new Services_JSON(); - $this->printText($json->encode($this->getResultData(), $this->getIsHtml())); + $this->printText($prefix . $json->encode($this->getResultData(), $this->getIsHtml()) . $suffix); } else { - $this->printText(json_encode($this->getResultData())); + $this->printText($prefix . json_encode($this->getResultData()) . $suffix); } } + protected function getAllowedParams() { + return array ( + 'callback' => null + ); + } + + protected function getParamDescription() { + return array ( + 'callback' => 'If specified, wraps the output into a given function call', + ); + } + protected function getDescription() { if ($this->mIsRaw) return 'Output data with the debuging elements in JSON format' . parent :: getDescription(); @@ -65,7 +86,7 @@ class ApiFormatJson extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatJson.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatJson.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php index 2cd87930..a8c649c3 100644 --- a/includes/api/ApiFormatJson_json.php +++ b/includes/api/ApiFormatJson_json.php @@ -843,4 +843,4 @@ if (class_exists('PEAR_Error')) { } -?> + diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index add63362..766d7041 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,7 +50,7 @@ class ApiFormatPhp extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatPhp.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatPhp.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index bc720490..0ddfac73 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -85,7 +85,7 @@ class ApiFormatWddx extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatWddx.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatWddx.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 7d54b441..02647923 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -141,7 +141,7 @@ class ApiFormatXml extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatXml.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatXml.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index 0107eb2b..400c0a4b 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -50,7 +50,7 @@ class ApiFormatYaml extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatYaml.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiFormatYaml.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php index a67bbb22..b3ccff0f 100644 --- a/includes/api/ApiFormatYaml_spyc.php +++ b/includes/api/ApiFormatYaml_spyc.php @@ -857,4 +857,4 @@ return $ret; } } -?> + diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 7c5144fd..9f1e88ea 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * This is a simple class to handle action=help + * * @addtogroup API */ class ApiHelp extends ApiBase { @@ -51,7 +53,7 @@ class ApiHelp extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiHelp.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiHelp.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 147d37a1..af68b29d 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -5,7 +5,8 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006-2007 Yuri Astrakhan @gmail.com, + * Daniel Cannon (cannon dot danielc at gmail dot com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,18 +30,60 @@ if (!defined('MEDIAWIKI')) { } /** + * Unit to authenticate log-in attempts to the current wiki. + * * @addtogroup API */ class ApiLogin extends ApiBase { - + + /** + * Time (in seconds) a user must wait after submitting + * a bad login (will be multiplied by the THROTTLE_FACTOR for each bad attempt) + */ + const THROTTLE_TIME = 1; + + /** + * The factor by which the wait-time in between authentication + * attempts is increased every failed attempt. + */ + const THROTTLE_FACTOR = 2; + + /** + * The maximum number of failed logins after which the wait increase stops. + */ + const THOTTLE_MAX_COUNT = 10; + public function __construct($main, $action) { parent :: __construct($main, $action, 'lg'); } + /** + * Executes the log-in attempt using the parameters passed. If + * the log-in succeeeds, it attaches a cookie to the session + * and outputs the user id, username, and session token. If a + * log-in fails, as the result of a bad password, a nonexistant + * user, or any other reason, the host is cached with an expiry + * and no log-in attempts will be accepted until that expiry + * is reached. The expiry is $this->mLoginThrottle. + * + * @access public + */ public function execute() { $name = $password = $domain = null; extract($this->extractRequestParams()); + $result = array (); + + // Make sure noone is trying to guess the password brut-force + $nextLoginIn = $this->getNextLoginTimeout(); + if ($nextLoginIn > 0) { + $result['result'] = 'NeedToWait'; + $result['details'] = "Please wait $nextLoginIn seconds before next log-in attempt"; + $result['wait'] = $nextLoginIn; + $this->getResult()->addValue(null, 'login', $result); + return; + } + $params = new FauxRequest(array ( 'wpName' => $name, 'wpPassword' => $password, @@ -48,8 +91,6 @@ class ApiLogin extends ApiBase { 'wpRemember' => '' )); - $result = array (); - $loginForm = new LoginForm($params); switch ($loginForm->authenticateUserData()) { case LoginForm :: SUCCESS : @@ -86,9 +127,89 @@ class ApiLogin extends ApiBase { ApiBase :: dieDebug(__METHOD__, 'Unhandled case value'); } + if ($result['result'] != 'Success') { + $result['wait'] = $this->cacheBadLogin(); + } + // if we were allowed to try to login, memcache is fine + $this->getResult()->addValue(null, 'login', $result); } + + /** + * Caches a bad-login attempt associated with the host and with an + * expiry of $this->mLoginThrottle. These are cached by a key + * separate from that used by the captcha system--as such, logging + * in through the standard interface will get you a legal session + * and cookies to prove it, but will not remove this entry. + * + * Returns the number of seconds until next login attempt will be allowed. + * + * @access private + */ + private function cacheBadLogin() { + global $wgMemc; + + $key = $this->getMemCacheKey(); + $val = $wgMemc->get( $key ); + + $val['lastReqTime'] = time(); + if (!isset($val['count'])) { + $val['count'] = 1; + } else { + $val['count'] = 1 + $val['count']; + } + + $delay = ApiLogin::calculateDelay($val['count']); + + $wgMemc->delete($key); + // Cache expiration should be the maximum timeout - to prevent a "try and wait" attack + $wgMemc->add( $key, $val, ApiLogin::calculateDelay(ApiLogin::THOTTLE_MAX_COUNT) ); + + return $delay; + } + + /** + * How much time the client must wait before it will be + * allowed to try to log-in next. + * The return value is 0 if no wait is required. + */ + private function getNextLoginTimeout() { + global $wgMemc; + + $val = $wgMemc->get($this->getMemCacheKey()); + + $elapse = (time() - $val['lastReqTime']); // in seconds + $canRetryIn = ApiLogin::calculateDelay($val['count']) - $elapse; + + return $canRetryIn < 0 ? 0 : $canRetryIn; + } + + /** + * Based on the number of previously attempted logins, returns + * the delay (in seconds) when the next login attempt will be allowed. + */ + private static function calculateDelay($count) { + // Defensive programming + $count = intval($count); + $count = $count < 1 ? 1 : $count; + $count = $count > self::THOTTLE_MAX_COUNT ? self::THOTTLE_MAX_COUNT : $count; + + return self::THROTTLE_TIME + self::THROTTLE_TIME * ($count - 1) * self::THROTTLE_FACTOR; + } + + /** + * Internal cache key for badlogin checks. Robbed from the + * ConfirmEdit extension and modified to use a key unique to the + * API login.3 + * + * @return string + * @access private + */ + private function getMemCacheKey() { + return wfMemcKey( 'apilogin', 'badlogin', 'ip', wfGetIP() ); + } + protected function getAllowedParams() { return array ( 'name' => null, @@ -107,7 +228,11 @@ class ApiLogin extends ApiBase { protected function getDescription() { return array ( - 'This module is used to login and get the authentication tokens.' + 'This module is used to login and get the authentication tokens. ', + 'In the event of a successful log-in, a cookie will be attached', + 'to your session. In the event of a failed log-in, you will not ', + 'be able to attempt another log-in through this method for 60 seconds.', + 'This is to prevent password guessing by automated password crackers.' ); } @@ -118,7 +243,7 @@ class ApiLogin extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogin.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiLogin.php 24695 2007-08-09 09:53:05Z yurik $'; } } -?> + diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 9a6b0f83..31870449 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,7 +29,16 @@ if (!defined('MEDIAWIKI')) { } /** - * This is the main API class, used for both external and internal processing. + * This is the main API class, used for both external and internal processing. + * When executed, it will create the requested formatter object, + * instantiate and execute an object associated with the needed action, + * and use formatter to print results. + * In case of an exception, an error message will be printed using the same formatter. + * + * To use API from another application, run it using FauxRequest object, in which + * case any internal exceptions will not be handled but passed up to the caller. + * After successful execution, use getResult() for the resulting data. + * * @addtogroup API */ class ApiMain extends ApiBase { @@ -43,11 +52,11 @@ class ApiMain extends ApiBase { * List of available modules: action name => module class */ private static $Modules = array ( - 'help' => 'ApiHelp', 'login' => 'ApiLogin', + 'query' => 'ApiQuery', 'opensearch' => 'ApiOpenSearch', 'feedwatchlist' => 'ApiFeedWatchlist', - 'query' => 'ApiQuery' + 'help' => 'ApiHelp', ); /** @@ -68,10 +77,11 @@ class ApiMain extends ApiBase { ); private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; - private $mResult, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage; + private $mResult, $mAction, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode, $mSquidMaxage; /** - * Constructor + * Constructs an instance of ApiMain that utilizes the module and format specified by $request. + * * @param $request object - if this is an instance of FauxRequest, errors are thrown and no printing occurs * @param $enableWrite bool should be set to true if the api may modify data */ @@ -82,7 +92,23 @@ class ApiMain extends ApiBase { // Special handling for the main module: $parent === $this parent :: __construct($this, $this->mInternalMode ? 'main_int' : 'main'); - $this->mModules = self :: $Modules; + if (!$this->mInternalMode) { + + // Impose module restrictions. + // If the current user cannot read, + // Remove all modules other than login + global $wgUser; + if (!$wgUser->isAllowed('read')) { + self::$Modules = array( + 'login' => self::$Modules['login'], + 'help' => self::$Modules['help'] + ); + } + } + + global $wgAPIModules; // extension modules + $this->mModules = $wgAPIModules + self :: $Modules; + $this->mModuleNames = array_keys($this->mModules); // todo: optimize $this->mFormats = self :: $Formats; $this->mFormatNames = array_keys($this->mFormats); // todo: optimize @@ -96,28 +122,53 @@ class ApiMain extends ApiBase { $this->mSquidMaxage = 0; } - public function & getRequest() { + /** + * Return true if the API was started by other PHP code using FauxRequest + */ + public function isInternalMode() { + return $this->mInternalMode; + } + + /** + * Return the request object that contains client's request + */ + public function getRequest() { return $this->mRequest; } + /** + * Get the ApiResult object asscosiated with current request + */ public function getResult() { return $this->mResult; } + /** + * This method will simply cause an error if the write mode was disabled for this api. + */ public function requestWriteMode() { if (!$this->mEnableWrite) $this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' . 'statement is included in the site\'s LocalSettings.php file', 'readonly'); } + /** + * Set how long the response should be cached. + */ public function setCacheMaxAge($maxage) { $this->mSquidMaxage = $maxage; } + /** + * Create an instance of an output formatter by its name + */ public function createPrinterByName($format) { return new $this->mFormats[$format] ($this, $format); } + /** + * Execute api request. Any errors will be handled if the API was called by the remote client. + */ public function execute() { $this->profileIn(); if ($this->mInternalMode) @@ -127,10 +178,14 @@ class ApiMain extends ApiBase { $this->profileOut(); } + /** + * Execute an action, and in case of an error, erase whatever partial results + * have been accumulated, and replace it with an error message and a help screen. + */ protected function executeActionWithErrorHandling() { // In case an error occurs during data output, - // this clear the output buffer and print just the error information + // clear the output buffer and print just the error information ob_start(); try { @@ -142,12 +197,51 @@ class ApiMain extends ApiBase { // handler will process and log it. // + $errCode = $this->substituteResultWithError($e); + // Error results should not be cached $this->setCacheMaxAge(0); + $headerStr = 'MediaWiki-API-Error: ' . $errCode; + if ($e->getCode() === 0) + header($headerStr, true); + else + header($headerStr, true, $e->getCode()); + + // Reset and print just the error message + ob_clean(); + + // If the error occured during printing, do a printer->profileOut() + $this->mPrinter->safeProfileOut(); + $this->printResult(true); + } + + // Set the cache expiration at the last moment, as any errors may change the expiration. + // if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch + $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage; + header('Expires: ' . wfTimestamp(TS_RFC2822, $expires)); + header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0'); + + if($this->mPrinter->getIsHtml()) + echo wfReportTime(); + + ob_end_flush(); + } + + /** + * Replace the result data with the information about an exception. + * Returns the error code + */ + protected function substituteResultWithError($e) { + // Printer may not be initialized if the extractRequestParams() fails for the main module if (!isset ($this->mPrinter)) { - $this->mPrinter = $this->createPrinterByName(self :: API_DEFAULT_FORMAT); + // The printer has not been created yet. Try to manually get formatter value. + $value = $this->getRequest()->getVal('format', self::API_DEFAULT_FORMAT); + if (!in_array($value, $this->mFormatNames)) + $value = self::API_DEFAULT_FORMAT; + + $this->mPrinter = $this->createPrinterByName($value); if ($this->mPrinter->getNeedsRawData()) $this->getResult()->setRawMode(); } @@ -157,8 +251,12 @@ class ApiMain extends ApiBase { // User entered incorrect parameters - print usage screen // $errMessage = array ( - 'code' => $e->getCodeString(), 'info' => $e->getMessage()); - ApiResult :: setContent($errMessage, $this->makeHelpMsg()); + 'code' => $e->getCodeString(), + 'info' => $e->getMessage()); + + // Only print the help message when this is for the developer, not runtime + if ($this->mPrinter->getIsHtml() || $this->mAction == 'help') + ApiResult :: setContent($errMessage, $this->makeHelpMsg()); } else { // @@ -171,41 +269,24 @@ class ApiMain extends ApiBase { ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n"); } - $headerStr = 'MediaWiki-API-Error: ' . $errMessage['code']; - if ($e->getCode() === 0) - header($headerStr, true); - else - header($headerStr, true, $e->getCode()); - - // Reset and print just the error message - ob_clean(); $this->getResult()->reset(); $this->getResult()->addValue(null, 'error', $errMessage); - // If the error occured during printing, do a printer->profileOut() - $this->mPrinter->safeProfileOut(); - $this->printResult(true); - } - - // Set the cache expiration at the last moment, as any errors may change the expiration. - // if $this->mSquidMaxage == 0, the expiry time is set to the first second of unix epoch - $expires = $this->mSquidMaxage == 0 ? 1 : time() + $this->mSquidMaxage; - header('Expires: ' . wfTimestamp(TS_RFC2822, $expires)); - header('Cache-Control: s-maxage=' . $this->mSquidMaxage . ', must-revalidate, max-age=0'); - - ob_end_flush(); + return $errMessage['code']; } /** * Execute the actual module, without any error handling */ protected function executeAction() { - $action = $format = $version = null; - extract($this->extractRequestParams()); - $this->mShowVersions = $version; + + $params = $this->extractRequestParams(); + + $this->mShowVersions = $params['version']; + $this->mAction = $params['action']; // Instantiate the module requested by the user - $module = new $this->mModules[$action] ($this, $action); + $module = new $this->mModules[$this->mAction] ($this, $this->mAction); if (!$this->mInternalMode) { @@ -213,7 +294,7 @@ class ApiMain extends ApiBase { $this->mPrinter = $module->getCustomPrinter(); if (is_null($this->mPrinter)) { // Create an appropriate printer - $this->mPrinter = $this->createPrinterByName($format); + $this->mPrinter = $this->createPrinterByName($params['format']); } if ($this->mPrinter->getNeedsRawData()) @@ -232,7 +313,7 @@ class ApiMain extends ApiBase { } /** - * Internal printer + * Print results using the current printer */ protected function printResult($isError) { $printer = $this->mPrinter; @@ -243,6 +324,9 @@ class ApiMain extends ApiBase { $printer->profileOut(); } + /** + * See ApiBase for description. + */ protected function getAllowedParams() { return array ( 'format' => array ( @@ -257,6 +341,9 @@ class ApiMain extends ApiBase { ); } + /** + * See ApiBase for description. + */ protected function getParamDescription() { return array ( 'format' => 'The format of the output', @@ -265,24 +352,44 @@ class ApiMain extends ApiBase { ); } + /** + * See ApiBase for description. + */ protected function getDescription() { return array ( '', - 'This API allows programs to access various functions of MediaWiki software.', - 'For more details see API Home Page @ http://meta.wikimedia.org/wiki/API', '', - 'Status: ALPHA -- all features shown on this page should be working,', - ' but the API is still in active development, and may change at any time.', - ' Make sure you monitor changes to this page, wikitech-l mailing list,', - ' or the source code in the includes/api directory for any changes.', - '' + '******************************************************************', + '** **', + '** This is an auto-generated MediaWiki API documentation page **', + '** **', + '** Documentation and Examples: **', + '** http://www.mediawiki.org/wiki/API **', + '** **', + '******************************************************************', + '', + 'Status: All features shown on this page should be working, but the API', + ' is still in active development, and may change at any time.', + ' Make sure to monitor our mailing list for any updates.', + '', + 'Documentation: http://www.mediawiki.org/wiki/API', + 'Mailing list: http://lists.wikimedia.org/mailman/listinfo/mediawiki-api', + 'Bugs & Requests: http://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts', + '', + '', + '', + '', + '', ); } + /** + * Returns an array of strings with credits for the API + */ protected function getCredits() { return array( - 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / FirstnameLastname@gmail.com', - 'Please leave your comments and suggestions at http://meta.wikimedia.org/wiki/API' + 'This API is being implemented by Yuri Astrakhan [[User:Yurik]] / @gmail.com', + 'Please leave your comments and suggestions at http://www.mediawiki.org/wiki/API' ); } @@ -297,8 +404,8 @@ class ApiMain extends ApiBase { $astriks = str_repeat('*** ', 10); $msg .= "\n\n$astriks Modules $astriks\n\n"; foreach( $this->mModules as $moduleName => $unused ) { - $msg .= "* action=$moduleName *"; $module = new $this->mModules[$moduleName] ($this, $moduleName); + $msg .= self::makeHelpMsgHeader($module, 'action'); $msg2 = $module->makeHelpMsg(); if ($msg2 !== false) $msg .= $msg2; @@ -307,8 +414,8 @@ class ApiMain extends ApiBase { $msg .= "\n$astriks Formats $astriks\n\n"; foreach( $this->mFormats as $formatName => $unused ) { - $msg .= "* format=$formatName *"; $module = $this->createPrinterByName($formatName); + $msg .= self::makeHelpMsgHeader($module, 'format'); $msg2 = $module->makeHelpMsg(); if ($msg2 !== false) $msg .= $msg2; @@ -321,7 +428,21 @@ class ApiMain extends ApiBase { return $msg; } + public static function makeHelpMsgHeader($module, $paramName) { + $modulePrefix = $module->getModulePrefix(); + if (!empty($modulePrefix)) + $modulePrefix = "($modulePrefix) "; + + return "* $paramName={$module->getModuleName()} $modulePrefix*"; + } + private $mIsBot = null; + + private $mIsSysop = null; + + /** + * Returns true if the currently logged in user is a bot, false otherwise + */ public function isBot() { if (!isset ($this->mIsBot)) { global $wgUser; @@ -329,24 +450,69 @@ class ApiMain extends ApiBase { } return $this->mIsBot; } + + /** + * Similar to isBot(), this method returns true if the logged in user is + * a sysop, and false if not. + */ + public function isSysop() { + if (!isset ($this->mIsSysop)) { + global $wgUser; + $this->mIsSysop = in_array( 'sysop', $wgUser->getGroups()); + } + + return $this->mIsSysop; + } public function getShowVersions() { return $this->mShowVersions; } + /** + * Returns the version information of this file, plus it includes + * the versions for all files that are not callable proper API modules + */ public function getVersion() { $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiMain.php 21402 2007-04-20 08:55:14Z nickj $'; + $vers[] = 'MediaWiki ' . SpecialVersion::getVersion(); + $vers[] = __CLASS__ . ': $Id: ApiMain.php 25364 2007-08-31 15:23:48Z tstarling $'; $vers[] = ApiBase :: getBaseVersion(); $vers[] = ApiFormatBase :: getBaseVersion(); $vers[] = ApiQueryBase :: getBaseVersion(); $vers[] = ApiFormatFeedWrapper :: getVersion(); // not accessible with format=xxx return $vers; } + + /** + * Add or overwrite a module in this ApiMain instance. Intended for use by extending + * classes who wish to add their own modules to their lexicon or override the + * behavior of inherent ones. + * + * @access protected + * @param $mdlName String The identifier for this module. + * @param $mdlClass String The class where this module is implemented. + */ + protected function addModule( $mdlName, $mdlClass ) { + $this->mModules[$mdlName] = $mdlClass; + } + + /** + * Add or overwrite an output format for this ApiMain. Intended for use by extending + * classes who wish to add to or modify current formatters. + * + * @access protected + * @param $fmtName The identifier for this format. + * @param $fmtClass The class implementing this format. + */ + protected function addFormat( $fmtName, $fmtClass ) { + $this->mFormats[$fmtName] = $fmtClass; + } } /** * This exception will be thrown when dieUsage is called to stop module execution. + * The exception handling code will print a help screen explaining how this API may be used. + * * @addtogroup API */ class UsageException extends Exception { @@ -364,4 +530,4 @@ class UsageException extends Exception { return "{$this->getCodeString()}: {$this->getMessage()}"; } } -?> + diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 77f8b889..8484b163 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,8 +42,8 @@ class ApiOpenSearch extends ApiBase { } public function execute() { - $search = null; - extract($this->ExtractRequestParams()); + $params = $this->extractRequestParams(); + $search = $params['search']; // Open search results may be stored for a very long time $this->getMain()->setCacheMaxAge(1200); @@ -53,7 +53,7 @@ class ApiOpenSearch extends ApiBase { return; // Return empty result // Prepare nested request - $params = new FauxRequest(array ( + $req = new FauxRequest(array ( 'action' => 'query', 'list' => 'allpages', 'apnamespace' => $title->getNamespace(), @@ -62,7 +62,7 @@ class ApiOpenSearch extends ApiBase { )); // Execute - $module = new ApiMain($params); + $module = new ApiMain($req); $module->execute(); // Get resulting data @@ -105,7 +105,7 @@ class ApiOpenSearch extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiOpenSearch.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiOpenSearch.php 24099 2007-07-15 00:52:35Z yurik $'; } } -?> + diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index dea87b88..185c0c59 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,14 +29,25 @@ if (!defined('MEDIAWIKI')) { } /** + * This class contains a list of pages that the client has requested. + * Initially, when the client passes in titles=, pageids=, or revisions= parameter, + * an instance of the ApiPageSet class will normalize titles, + * determine if the pages/revisions exist, and prefetch any additional data page data requested. + * + * When generator is used, the result of the generator will become the input for the + * second instance of this class, and all subsequent actions will go use the second instance + * for all their work. + * * @addtogroup API */ class ApiPageSet extends ApiQueryBase { private $mAllPages; // [ns][dbkey] => page_id or 0 when missing - private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles, $mNormalizedTitles; + private $mTitles, $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles; + private $mNormalizedTitles, $mInterwikiTitles; private $mResolveRedirects, $mPendingRedirectIDs; private $mGoodRevIDs, $mMissingRevIDs; + private $mFakePageId; private $mRequestedPageFields; @@ -50,6 +61,7 @@ class ApiPageSet extends ApiQueryBase { $this->mMissingPageIDs = array (); $this->mRedirectTitles = array (); $this->mNormalizedTitles = array (); + $this->mInterwikiTitles = array (); $this->mGoodRevIDs = array(); $this->mMissingRevIDs = array(); @@ -57,6 +69,8 @@ class ApiPageSet extends ApiQueryBase { $this->mResolveRedirects = $resolveRedirects; if($resolveRedirects) $this->mPendingRedirectIDs = array(); + + $this->mFakePageId = -1; } public function isResolvingRedirects() { @@ -88,7 +102,16 @@ class ApiPageSet extends ApiQueryBase { if ($this->mResolveRedirects) $pageFlds['page_is_redirect'] = null; - return array_keys(array_merge($pageFlds, $this->mRequestedPageFields)); + $pageFlds = array_merge($pageFlds, $this->mRequestedPageFields); + return array_keys($pageFlds); + } + + /** + * Returns an array [ns][dbkey] => page_id for all requested titles + * page_id is a unique negative number in case title was not found + */ + public function getAllTitlesByNamespace() { + return $this->mAllPages; } /** @@ -123,6 +146,7 @@ class ApiPageSet extends ApiQueryBase { /** * Title objects that were NOT found in the database. + * The array's index will be negative for each item * @return array of Title objects */ public function getMissingTitles() { @@ -154,6 +178,15 @@ class ApiPageSet extends ApiQueryBase { return $this->mNormalizedTitles; } + /** + * Get a list of interwiki titles - maps the title given + * with to the interwiki prefix. + * @return array raw_prefixed_title (string) => interwiki_prefix (string) + */ + public function getInterwikiTitles() { + return $this->mInterwikiTitles; + } + /** * Get the list of revision IDs (requested with revids= parameter) * @return array revID (int) => pageID (int) @@ -233,7 +266,6 @@ class ApiPageSet extends ApiQueryBase { */ public function populateFromPageIDs($pageIDs) { $this->profileIn(); - $pageIDs = array_map('intval', $pageIDs); // paranoia $this->initFromPageIds($pageIDs); $this->profileOut(); } @@ -265,22 +297,18 @@ class ApiPageSet extends ApiQueryBase { // Store Title object in various data structures $title = Title :: makeTitle($row->page_namespace, $row->page_title); - // skip any pages that user has no rights to read - if ($title->userCanRead()) { - - $pageId = intval($row->page_id); - $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; - $this->mTitles[] = $title; - - if ($this->mResolveRedirects && $row->page_is_redirect == '1') { - $this->mPendingRedirectIDs[$pageId] = $title; - } else { - $this->mGoodTitles[$pageId] = $title; - } - - foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) - $fieldValues[$pageId] = $row-> $fieldName; + $pageId = intval($row->page_id); + $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; + $this->mTitles[] = $title; + + if ($this->mResolveRedirects && $row->page_is_redirect == '1') { + $this->mPendingRedirectIDs[$pageId] = $title; + } else { + $this->mGoodTitles[$pageId] = $title; } + + foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) + $fieldValues[$pageId] = $row-> $fieldName; } public function finishPageSetGeneration() { @@ -306,7 +334,7 @@ class ApiPageSet extends ApiQueryBase { private function initFromTitles($titles) { // Get validated and normalized title objects - $linkBatch = $this->processTitlesStrArray($titles); + $linkBatch = $this->processTitlesArray($titles); if($linkBatch->isEmpty()) return; @@ -328,7 +356,8 @@ class ApiPageSet extends ApiQueryBase { private function initFromPageIds($pageids) { if(empty($pageids)) return; - + + $pageids = array_map('intval', $pageids); // paranoia $set = array ( 'page_id' => $pageids ); @@ -386,8 +415,9 @@ class ApiPageSet extends ApiQueryBase { foreach ($remaining as $ns => $dbkeys) { foreach ( $dbkeys as $dbkey => $unused ) { $title = Title :: makeTitle($ns, $dbkey); - $this->mMissingTitles[] = $title; - $this->mAllPages[$ns][$dbkey] = 0; + $this->mAllPages[$ns][$dbkey] = $this->mFakePageId; + $this->mMissingTitles[$this->mFakePageId] = $title; + $this->mFakePageId--; $this->mTitles[] = $title; } } @@ -536,39 +566,46 @@ class ApiPageSet extends ApiQueryBase { /** * Given an array of title strings, convert them into Title objects. + * Alternativelly, an array of Title objects may be given. * This method validates access rights for the title, * and appends normalization values to the output. * * @return LinkBatch of title objects. */ - private function processTitlesStrArray($titles) { + private function processTitlesArray($titles) { $linkBatch = new LinkBatch(); - foreach ($titles as $titleString) { - $titleObj = Title :: newFromText($titleString); - - // Validation + foreach ($titles as $title) { + + $titleObj = is_string($title) ? Title :: newFromText($title) : $title; if (!$titleObj) - $this->dieUsage("bad title $titleString", 'invalidtitle'); - if ($titleObj->getNamespace() < 0) - $this->dieUsage("No support for special page $titleString has been implemented", 'unsupportednamespace'); - if (!$titleObj->userCanRead()) - $this->dieUsage("No read permission for $titleString", 'titleaccessdenied'); + $this->dieUsage("bad title", 'invalidtitle'); - $linkBatch->addObj($titleObj); + $iw = $titleObj->getInterwiki(); + if (!empty($iw)) { + // This title is an interwiki link. + $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw; + } else { + // Validation + if ($titleObj->getNamespace() < 0) + $this->dieUsage("No support for special pages has been implemented", 'unsupportednamespace'); + + $linkBatch->addObj($titleObj); + } + // Make sure we remember the original title that was given to us // This way the caller can correlate new titles with the originally requested, // i.e. namespace is localized or capitalization is different - if ($titleString !== $titleObj->getPrefixedText()) { - $this->mNormalizedTitles[$titleString] = $titleObj->getPrefixedText(); + if (is_string($title) && $title !== $titleObj->getPrefixedText()) { + $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText(); } } return $linkBatch; } - + protected function getAllowedParams() { return array ( 'titles' => array ( @@ -594,7 +631,7 @@ class ApiPageSet extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPageSet.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiPageSet.php 24935 2007-08-20 08:13:16Z nickj $'; } } -?> + diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 6ee05085..76dbb338 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,49 +29,67 @@ if (!defined('MEDIAWIKI')) { } /** + * This is the main query class. It behaves similar to ApiMain: based on the parameters given, + * it will create a list of titles to work on (an instance of the ApiPageSet object) + * instantiate and execute various property/list/meta modules, + * and assemble all resulting data into a single ApiResult object. + * + * In the generator mode, a generator will be first executed to populate a second ApiPageSet object, + * and that object will be used for all subsequent modules. + * * @addtogroup API */ class ApiQuery extends ApiBase { private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames; private $mPageSet; + private $params, $redirect; private $mQueryPropModules = array ( 'info' => 'ApiQueryInfo', - 'revisions' => 'ApiQueryRevisions' + 'revisions' => 'ApiQueryRevisions', + 'links' => 'ApiQueryLinks', + 'langlinks' => 'ApiQueryLangLinks', + 'images' => 'ApiQueryImages', + 'imageinfo' => 'ApiQueryImageInfo', + 'templates' => 'ApiQueryLinks', + 'categories' => 'ApiQueryCategories', + 'extlinks' => 'ApiQueryExternalLinks', ); - // 'categories' => 'ApiQueryCategories', - // 'imageinfo' => 'ApiQueryImageinfo', - // 'langlinks' => 'ApiQueryLanglinks', - // 'links' => 'ApiQueryLinks', - // 'templates' => 'ApiQueryTemplates', private $mQueryListModules = array ( 'allpages' => 'ApiQueryAllpages', - 'logevents' => 'ApiQueryLogEvents', - 'watchlist' => 'ApiQueryWatchlist', - 'recentchanges' => 'ApiQueryRecentChanges', + 'alllinks' => 'ApiQueryAllLinks', + 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', + 'categorymembers' => 'ApiQueryCategoryMembers', 'embeddedin' => 'ApiQueryBacklinks', - 'imagelinks' => 'ApiQueryBacklinks', - 'usercontribs' => 'ApiQueryContributions' + 'imageusage' => 'ApiQueryBacklinks', + 'logevents' => 'ApiQueryLogEvents', + 'recentchanges' => 'ApiQueryRecentChanges', + 'search' => 'ApiQuerySearch', + 'usercontribs' => 'ApiQueryContributions', + 'watchlist' => 'ApiQueryWatchlist', + 'exturlusage' => 'ApiQueryExtLinksUsage', ); - // 'categorymembers' => 'ApiQueryCategorymembers', - // 'embeddedin' => 'ApiQueryEmbeddedin', - // 'imagelinks' => 'ApiQueryImagelinks', - // 'recentchanges' => 'ApiQueryRecentchanges', - // 'users' => 'ApiQueryUsers', - // 'watchlist' => 'ApiQueryWatchlist', private $mQueryMetaModules = array ( - 'siteinfo' => 'ApiQuerySiteinfo' + 'siteinfo' => 'ApiQuerySiteinfo', + 'userinfo' => 'ApiQueryUserInfo', ); - // 'userinfo' => 'ApiQueryUserinfo', private $mSlaveDB = null; + private $mNamedDB = array(); public function __construct($main, $action) { parent :: __construct($main, $action); + + // Allow custom modules to be added in LocalSettings.php + global $wgApiQueryPropModules, $wgApiQueryListModules, $wgApiQueryMetaModules; + self :: appendUserModules($this->mQueryPropModules, $wgApiQueryPropModules); + self :: appendUserModules($this->mQueryListModules, $wgApiQueryListModules); + self :: appendUserModules($this->mQueryMetaModules, $wgApiQueryMetaModules); + $this->mPropModuleNames = array_keys($this->mQueryPropModules); $this->mListModuleNames = array_keys($this->mQueryListModules); $this->mMetaModuleNames = array_keys($this->mQueryMetaModules); @@ -81,6 +99,20 @@ class ApiQuery extends ApiBase { $this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames); } + /** + * Helper function to append any add-in modules to the list + */ + private static function appendUserModules(&$modules, $newModules) { + if (is_array( $newModules )) { + foreach ( $newModules as $moduleName => $moduleClass) { + $modules[$moduleName] = $moduleClass; + } + } + } + + /** + * Gets a default slave database connection object + */ public function getDB() { if (!isset ($this->mSlaveDB)) { $this->profileDBIn(); @@ -90,6 +122,24 @@ class ApiQuery extends ApiBase { return $this->mSlaveDB; } + /** + * Get the query database connection with the given name. + * If no such connection has been requested before, it will be created. + * Subsequent calls with the same $name will return the same connection + * as the first, regardless of $db or $groups new values. + */ + public function getNamedDB($name, $db, $groups) { + if (!array_key_exists($name, $this->mNamedDB)) { + $this->profileDBIn(); + $this->mNamedDB[$name] = wfGetDB($db, $groups); + $this->profileDBOut(); + } + return $this->mNamedDB[$name]; + } + + /** + * Gets the set of pages the user has requested (or generated) + */ public function getPageSet() { return $this->mPageSet; } @@ -105,42 +155,33 @@ class ApiQuery extends ApiBase { * #5 Execute all requested modules */ public function execute() { - $prop = $list = $meta = $generator = $redirects = null; - extract($this->extractRequestParams()); - + + $this->params = $this->extractRequestParams(); + $this->redirects = $this->params['redirects']; + // // Create PageSet // - $this->mPageSet = new ApiPageSet($this, $redirects); - - // Instantiate required modules - $modules = array (); - if (isset ($prop)) - foreach ($prop as $moduleName) - $modules[] = new $this->mQueryPropModules[$moduleName] ($this, $moduleName); - if (isset ($list)) - foreach ($list as $moduleName) - $modules[] = new $this->mQueryListModules[$moduleName] ($this, $moduleName); - if (isset ($meta)) - foreach ($meta as $moduleName) - $modules[] = new $this->mQueryMetaModules[$moduleName] ($this, $moduleName); - - // Modules may optimize data requests through the $this->getPageSet() object - // Execute all requested modules. - foreach ($modules as $module) { - $module->requestExtraData(); - } + $this->mPageSet = new ApiPageSet($this, $this->redirects); // - // If given, execute generator to substitute user supplied data with generated data. + // Instantiate requested modules // - if (isset ($generator)) - $this->executeGeneratorModule($generator, $redirects); + $modules = array (); + $this->InstantiateModules($modules, 'prop', $this->mQueryPropModules); + $this->InstantiateModules($modules, 'list', $this->mQueryListModules); + $this->InstantiateModules($modules, 'meta', $this->mQueryMetaModules); // - // Populate page information for the given pageSet + // If given, execute generator to substitute user supplied data with generated data. // - $this->mPageSet->execute(); + if (isset ($this->params['generator'])) { + $this->executeGeneratorModule($this->params['generator'], $modules); + } else { + // Append custom fields and populate page/revision information + $this->addCustomFldsToPageSet($modules, $this->mPageSet); + $this->mPageSet->execute(); + } // // Record page information (title, namespace, if exists, etc) @@ -156,7 +197,33 @@ class ApiQuery extends ApiBase { $module->profileOut(); } } + + /** + * Query modules may optimize data requests through the $this->getPageSet() object + * by adding extra fields from the page table. + * This function will gather all the extra request fields from the modules. + */ + private function addCustomFldsToPageSet($modules, $pageSet) { + // Query all requested modules. + foreach ($modules as $module) { + $module->requestExtraData($pageSet); + } + } + + /** + * Create instances of all modules requested by the client + */ + private function InstantiateModules(&$modules, $param, $moduleList) { + $list = $this->params[$param]; + if (isset ($list)) + foreach ($list as $moduleName) + $modules[] = new $moduleList[$moduleName] ($this, $moduleName); + } + /** + * Appends an element for each page in the current pageSet with the most general + * information (id, title), plus any title normalizations and missing title/pageids/revids. + */ private function outputGeneralPageInfo() { $pageSet = $this->getPageSet(); @@ -175,7 +242,21 @@ class ApiQuery extends ApiBase { $result->setIndexedTagName($normValues, 'n'); $result->addValue('query', 'normalized', $normValues); } + + // Interwiki titles + $intrwValues = array (); + foreach ($pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr) { + $intrwValues[] = array ( + 'title' => $rawTitleStr, + 'iw' => $interwikiStr + ); + } + if (!empty ($intrwValues)) { + $result->setIndexedTagName($intrwValues, 'i'); + $result->addValue('query', 'interwiki', $intrwValues); + } + // Show redirect information $redirValues = array (); foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) { @@ -211,10 +292,11 @@ class ApiQuery extends ApiBase { $pages = array (); // Report any missing titles - $fakepageid = -1; - foreach ($pageSet->getMissingTitles() as $title) { - $pages[$fakepageid--] = array ( - 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'missing' => ''); + foreach ($pageSet->getMissingTitles() as $fakeId => $title) { + $vals = array(); + ApiQueryBase :: addTitleInfo($vals, $title); + $vals['missing'] = ''; + $pages[$fakeId] = $vals; } // Report any missing page ids @@ -227,32 +309,43 @@ class ApiQuery extends ApiBase { // Output general page information for found titles foreach ($pageSet->getGoodTitles() as $pageid => $title) { - $pages[$pageid] = array ( - 'pageid' => $pageid, - 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText()); + $vals = array(); + $vals['pageid'] = $pageid; + ApiQueryBase :: addTitleInfo($vals, $title); + $pages[$pageid] = $vals; } if (!empty ($pages)) { + + if ($this->params['indexpageids']) { + $pageIDs = array_keys($pages); + // json treats all map keys as strings - converting to match + $pageIDs = array_map('strval', $pageIDs); + $result->setIndexedTagName($pageIDs, 'id'); + $result->addValue('query', 'pageids', $pageIDs); + } + $result->setIndexedTagName($pages, 'page'); $result->addValue('query', 'pages', $pages); } } - protected function executeGeneratorModule($generatorName, $redirects) { + /** + * For generator mode, execute generator, and use its output as new pageSet + */ + protected function executeGeneratorModule($generatorName, $modules) { // Find class that implements requested generator if (isset ($this->mQueryListModules[$generatorName])) { $className = $this->mQueryListModules[$generatorName]; - } - elseif (isset ($this->mQueryPropModules[$generatorName])) { + } elseif (isset ($this->mQueryPropModules[$generatorName])) { $className = $this->mQueryPropModules[$generatorName]; } else { ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName"); } - // Use current pageset as the result, and create a new one just for the generator - $resultPageSet = $this->mPageSet; - $this->mPageSet = new ApiPageSet($this, $redirects); + // Generator results + $resultPageSet = new ApiPageSet($this, $this->redirects); // Create and execute the generator $generator = new $className ($this, $generatorName); @@ -260,9 +353,12 @@ class ApiQuery extends ApiBase { $this->dieUsage("Module $generatorName cannot be used as a generator", "badgenerator"); $generator->setGeneratorMode(); - $generator->requestExtraData(); - // execute current pageSet to get the data for the generator module + // Add any additional fields modules may need + $generator->requestExtraData($this->mPageSet); + $this->addCustomFldsToPageSet($modules, $resultPageSet); + + // Populate page information with the original user input $this->mPageSet->execute(); // populate resultPageSet with the generator output @@ -275,6 +371,10 @@ class ApiQuery extends ApiBase { $this->mPageSet = $resultPageSet; } + /** + * Returns the list of allowed parameters for this module. + * Qurey module also lists all ApiPageSet parameters as its own. + */ protected function getAllowedParams() { return array ( 'prop' => array ( @@ -292,7 +392,8 @@ class ApiQuery extends ApiBase { 'generator' => array ( ApiBase :: PARAM_TYPE => $this->mAllowedGenerators ), - 'redirects' => false + 'redirects' => false, + 'indexpageids' => false, ); } @@ -301,12 +402,12 @@ class ApiQuery extends ApiBase { */ public function makeHelpMsg() { - // Use parent to make default message for the query module - $msg = parent :: makeHelpMsg(); + $msg = ''; // Make sure the internal object is empty // (just in case a sub-module decides to optimize during instantiation) $this->mPageSet = null; + $this->mAllowedGenerators = array(); // Will be repopulated $astriks = str_repeat('--- ', 8); $msg .= "\n$astriks Query: Prop $astriks\n\n"; @@ -316,21 +417,32 @@ class ApiQuery extends ApiBase { $msg .= "\n$astriks Query: Meta $astriks\n\n"; $msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta'); + // Perform the base call last because the $this->mAllowedGenerators + // will be updated inside makeHelpMsgHelper() + // Use parent to make default message for the query module + $msg = parent :: makeHelpMsg() . $msg; + return $msg; } + /** + * For all modules in $moduleList, generate help messages and join them together + */ private function makeHelpMsgHelper($moduleList, $paramName) { $moduleDscriptions = array (); foreach ($moduleList as $moduleName => $moduleClass) { - $msg = "* $paramName=$moduleName *"; $module = new $moduleClass ($this, $moduleName, null); + + $msg = ApiMain::makeHelpMsgHeader($module, $paramName); $msg2 = $module->makeHelpMsg(); if ($msg2 !== false) $msg .= $msg2; - if ($module instanceof ApiQueryGeneratorBase) + if ($module instanceof ApiQueryGeneratorBase) { + $this->mAllowedGenerators[] = $moduleName; $msg .= "Generator:\n This module may be used as a generator\n"; + } $moduleDscriptions[] = $msg; } @@ -351,7 +463,8 @@ class ApiQuery extends ApiBase { 'list' => 'Which lists to get', 'meta' => 'Which meta data to get about the site', 'generator' => 'Use the output of a list as the input for other prop/list/meta items', - 'redirects' => 'Automatically resolve redirects' + 'redirects' => 'Automatically resolve redirects', + 'indexpageids' => 'Include an additional pageids section listing all returned page IDs.' ); } @@ -372,9 +485,9 @@ class ApiQuery extends ApiBase { public function getVersion() { $psModule = new ApiPageSet($this); $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiQuery.php 21402 2007-04-20 08:55:14Z nickj $'; + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 24494 2007-07-31 17:53:37Z yurik $'; $vers[] = $psModule->getVersion(); return $vers; } } -?> + diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php new file mode 100644 index 00000000..17f24b65 --- /dev/null +++ b/includes/api/ApiQueryAllLinks.php @@ -0,0 +1,179 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to enumerate links from all pages together. + * + * @addtogroup API + */ +class ApiQueryAllLinks extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'al'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + $db = $this->getDB(); + $params = $this->extractRequestParams(); + + $prop = array_flip($params['prop']); + $fld_ids = isset($prop['ids']); + $fld_title = isset($prop['title']); + + if ($params['unique']) { + if (!is_null($resultPageSet)) + $this->dieUsage($this->getModuleName() . ' cannot be used as a generator in unique links mode', 'params'); + if ($fld_ids) + $this->dieUsage($this->getModuleName() . ' cannot return corresponding page ids in unique links mode', 'params'); + $this->addOption('DISTINCT'); + } + + $this->addTables('pagelinks'); + $this->addWhereFld('pl_namespace', $params['namespace']); + + if (!is_null($params['from'])) + $this->addWhere('pl_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + if (isset ($params['prefix'])) + $this->addWhere("pl_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + + if (is_null($resultPageSet)) { + $this->addFields(array ( + 'pl_namespace', + 'pl_title' + )); + $this->addFieldsIf('pl_from', $fld_ids); + } else { + $this->addFields('pl_from'); + $pageids = array(); + } + + $this->addOption('USE INDEX', 'pl_namespace'); + $limit = $params['limit']; + $this->addOption('LIMIT', $limit+1); + $this->addOption('ORDER BY', 'pl_namespace, pl_title'); + + $res = $this->select(__METHOD__); + + $data = array (); + $count = 0; + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // TODO: Security issue - if the user has no right to view next title, it will still be shown + $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->pl_title)); + break; + } + + if (is_null($resultPageSet)) { + $vals = array(); + if ($fld_ids) + $vals['fromid'] = intval($row->pl_from); + if ($fld_title) { + $title = Title :: makeTitle($row->pl_namespace, $row->pl_title); + $vals['ns'] = intval($title->getNamespace()); + $vals['title'] = $title->getPrefixedText(); + } + $data[] = $vals; + } else { + $pageids[] = $row->pl_from; + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($data, 'l'); + $result->addValue('query', $this->getModuleName(), $data); + } else { + $resultPageSet->populateFromPageIDs($pageids); + } + } + + protected function getAllowedParams() { + return array ( + 'from' => null, + 'prefix' => null, + 'unique' => false, + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'title', + ApiBase :: PARAM_TYPE => array ( + 'ids', + 'title' + ) + ), + 'namespace' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'from' => 'The page title to start enumerating from.', + 'prefix' => 'Search for all page titles that begin with this value.', + 'unique' => 'Only show unique links. Cannot be used with generator or prop=ids', + 'prop' => 'What pieces of information to include', + 'namespace' => 'The namespace to enumerate.', + 'limit' => 'How many total links to return.' + ); + } + + protected function getDescription() { + return 'Enumerate all links that point to a given namespace'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=alllinks&alunique&alfrom=B', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllLinks.php 24453 2007-07-30 08:09:15Z yurik $'; + } +} diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php new file mode 100644 index 00000000..92bcc1a1 --- /dev/null +++ b/includes/api/ApiQueryAllUsers.php @@ -0,0 +1,204 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to enumerate all registered users. + * + * @addtogroup API + */ +class ApiQueryAllUsers extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'au'); + } + + public function execute() { + $db = $this->getDB(); + $params = $this->extractRequestParams(); + + $prop = $params['prop']; + if (!is_null($prop)) { + $prop = array_flip($prop); + $fld_editcount = isset($prop['editcount']); + $fld_groups = isset($prop['groups']); + } else { + $fld_editcount = $fld_groups = false; + } + + $limit = $params['limit']; + $tables = $db->tableName('user'); + + if( !is_null( $params['from'] ) ) + $this->addWhere( 'user_name >= ' . $db->addQuotes( self::keyToTitle( $params['from'] ) ) ); + + if( isset( $params['prefix'] ) ) + $this->addWhere( 'user_name LIKE "' . $db->escapeLike( self::keyToTitle( $params['prefix'] ) ) . '%"' ); + + if (!is_null($params['group'])) { + // Filter only users that belong to a given group + $tblName = $db->tableName('user_groups'); + $tables = "$tables INNER JOIN $tblName ug1 ON ug1.ug_user=user_id"; + $this->addWhereFld('ug1.ug_group', $params['group']); + } + + if ($fld_groups) { + // Show the groups the given users belong to + // request more than needed to avoid not getting all rows that belong to one user + $groupCount = count(User::getAllGroups()); + $sqlLimit = $limit+$groupCount+1; + + $tblName = $db->tableName('user_groups'); + $tables = "$tables LEFT JOIN $tblName ug2 ON ug2.ug_user=user_id"; + $this->addFields('ug2.ug_group ug_group2'); + } else { + $sqlLimit = $limit+1; + } + + $this->addOption('LIMIT', $sqlLimit); + $this->addTables($tables); + + $this->addFields('user_name'); + $this->addFieldsIf('user_editcount', $fld_editcount); + + $this->addOption('ORDER BY', 'user_name'); + + $res = $this->select(__METHOD__); + + $data = array (); + $count = 0; + $lastUserData = false; + $lastUser = false; + $result = $this->getResult(); + + // + // This loop keeps track of the last entry. + // For each new row, if the new row is for different user then the last, the last entry is added to results. + // Otherwise, the group of the new row is appended to the last entry. + // The setContinue... is more complex because of this, and takes into account the higher sql limit + // to make sure all rows that belong to the same user are received. + // + while (true) { + + $row = $db->fetchObject($res); + $count++; + + if (!$row || $lastUser != $row->user_name) { + // Save the last pass's user data + if (is_array($lastUserData)) + $data[] = $lastUserData; + + // No more rows left + if (!$row) + break; + + if ($count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->user_name)); + break; + } + + // Record new user's data + $lastUser = $row->user_name; + $lastUserData = array( 'name' => $lastUser ); + if ($fld_editcount) + $lastUserData['editcount'] = intval($row->user_editcount); + + } + + if ($sqlLimit == $count) { + // BUG! database contains group name that User::getAllGroups() does not return + // TODO: should handle this more gracefully + ApiBase :: dieDebug(__METHOD__, + 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function'); + } + + // Add user's group info + if ($fld_groups && !is_null($row->ug_group2)) { + $lastUserData['groups'][] = $row->ug_group2; + $result->setIndexedTagName($lastUserData['groups'], 'g'); + } + } + + $db->freeResult($res); + + $result->setIndexedTagName($data, 'u'); + $result->addValue('query', $this->getModuleName(), $data); + } + + protected function getAllowedParams() { + return array ( + 'from' => null, + 'prefix' => null, + 'group' => array( + ApiBase :: PARAM_TYPE => User::getAllGroups() + ), + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'editcount', + 'groups', + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'from' => 'The user name to start enumerating from.', + 'prefix' => 'Search for all page titles that begin with this value.', + 'group' => 'Limit users to a given group name', + 'prop' => array( + 'What pieces of information to include.', + '`groups` property uses more server resources and may return fewer results than the limit.'), + 'limit' => 'How many total user names to return.', + ); + } + + protected function getDescription() { + return 'Enumerate all registered users'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=allusers&aufrom=Y', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllUsers.php 24870 2007-08-17 13:01:35Z robchurch $'; + } +} diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 494f7707..d9715b1a 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * Query module to enumerate all available pages. + * * @addtogroup API */ class ApiQueryAllpages extends ApiQueryGeneratorBase { @@ -50,22 +52,51 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { private function run($resultPageSet = null) { - wfProfileIn($this->getModuleProfileName() . '-getDB'); $db = $this->getDB(); - wfProfileOut($this->getModuleProfileName() . '-getDB'); - - wfProfileIn($this->getModuleProfileName() . '-parseParams'); - $limit = $from = $namespace = $filterredir = $prefix = null; - extract($this->extractRequestParams()); + $params = $this->extractRequestParams(); + + // Page filters + if (!$this->addWhereIf('page_is_redirect = 1', $params['filterredir'] === 'redirects')) + $this->addWhereIf('page_is_redirect = 0', $params['filterredir'] === 'nonredirects'); + $this->addWhereFld('page_namespace', $params['namespace']); + if (!is_null($params['from'])) + $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($params['from']))); + if (isset ($params['prefix'])) + $this->addWhere("page_title LIKE '" . $db->escapeLike(ApiQueryBase :: titleToKey($params['prefix'])) . "%'"); + + $forceNameTitleIndex = true; + if (isset ($params['minsize'])) { + $this->addWhere('page_len>=' . intval($params['minsize'])); + $forceNameTitleIndex = false; + } + + if (isset ($params['maxsize'])) { + $this->addWhere('page_len<=' . intval($params['maxsize'])); + $forceNameTitleIndex = false; + } + + // Page protection filtering + if (isset ($params['prtype'])) { + $this->addTables('page_restrictions'); + $this->addWhere('page_id=pr_page'); + $this->addWhere('pr_expiry>' . $db->addQuotes($db->timestamp())); + $this->addWhereFld('pr_type', $params['prtype']); + + $prlevel = $params['prlevel']; + if (!is_null($prlevel) && $prlevel != '' && $prlevel != '*') + $this->addWhereFld('pr_level', $prlevel); + + $forceNameTitleIndex = false; + + } else if (isset ($params['prlevel'])) { + $this->dieUsage('prlevel may not be used without prtype', 'params'); + } + $this->addTables('page'); - if (!$this->addWhereIf('page_is_redirect = 1', $filterredir === 'redirects')) - $this->addWhereIf('page_is_redirect = 0', $filterredir === 'nonredirects'); - $this->addWhereFld('page_namespace', $namespace); - if (isset ($from)) - $this->addWhere('page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from))); - if (isset ($prefix)) - $this->addWhere("page_title LIKE '{$db->strencode(ApiQueryBase :: titleToKey($prefix))}%'"); + if ($forceNameTitleIndex) + $this->addOption('USE INDEX', 'name_title'); + if (is_null($resultPageSet)) { $this->addFields(array ( @@ -77,29 +108,28 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addFields($resultPageSet->getPageTableFields()); } - $this->addOption('USE INDEX', 'name_title'); - $this->addOption('LIMIT', $limit +1); + $limit = $params['limit']; + $this->addOption('LIMIT', $limit+1); $this->addOption('ORDER BY', 'page_namespace, page_title'); - wfProfileOut($this->getModuleProfileName() . '-parseParams'); - $res = $this->select(__METHOD__); - wfProfileIn($this->getModuleProfileName() . '-saveResults'); - $data = array (); $count = 0; while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // TODO: Security issue - if the user has no right to view next title, it will still be shown $this->setContinueEnumParameter('from', ApiQueryBase :: keyToTitle($row->page_title)); break; } if (is_null($resultPageSet)) { - $vals = $this->addRowInfo('page', $row); - if ($vals) - $data[intval($row->page_id)] = $vals; + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + $data[] = array( + 'pageid' => intval($row->page_id), + 'ns' => intval($title->getNamespace()), + 'title' => $title->getPrefixedText()); } else { $resultPageSet->processDbRow($row); } @@ -111,17 +141,17 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $result->setIndexedTagName($data, 'p'); $result->addValue('query', $this->getModuleName(), $data); } - - wfProfileOut($this->getModuleProfileName() . '-saveResults'); } protected function getAllowedParams() { + global $wgRestrictionTypes, $wgRestrictionLevels; + return array ( 'from' => null, 'prefix' => null, 'namespace' => array ( ApiBase :: PARAM_DFLT => 0, - ApiBase :: PARAM_TYPE => 'namespace' + ApiBase :: PARAM_TYPE => 'namespace', ), 'filterredir' => array ( ApiBase :: PARAM_DFLT => 'all', @@ -131,11 +161,23 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { 'nonredirects' ) ), + 'minsize' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ), + 'maxsize' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ), + 'prtype' => array ( + ApiBase :: PARAM_TYPE => $wgRestrictionTypes, + ), + 'prlevel' => array ( + ApiBase :: PARAM_TYPE => $wgRestrictionLevels, + ), 'limit' => array ( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ) ); @@ -147,6 +189,10 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { 'prefix' => 'Search for all page titles that begin with this value.', 'namespace' => 'The namespace to enumerate.', 'filterredir' => 'Which pages to list.', + 'minsize' => 'Limit to pages with at least this many bytes', + 'maxsize' => 'Limit to pages with at most this many bytes', + 'prtype' => 'Limit to protected pages only', + 'prlevel' => 'The protection level (must be used with apprtype= parameter)', 'limit' => 'How many total pages to return.' ); } @@ -169,7 +215,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllpages.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryAllpages.php 24694 2007-08-09 08:41:58Z yurik $'; } } -?> + diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 1a6783a9..a676b4bf 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,11 +29,16 @@ if (!defined('MEDIAWIKI')) { } /** + * This is three-in-one module to query: + * * backlinks - links pointing to the given page, + * * embeddedin - what pages transclude the given page within themselves, + * * imageusage - what pages use the given image + * * @addtogroup API */ class ApiQueryBacklinks extends ApiQueryGeneratorBase { - private $rootTitle, $contRedirs, $contLevel, $contTitle, $contID; + private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID; // output element name, database column field prefix, database table private $backlinksSettings = array ( @@ -47,8 +52,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { 'prefix' => 'tl', 'linktbl' => 'templatelinks' ), - 'imagelinks' => array ( - 'code' => 'il', + 'imageusage' => array ( + 'code' => 'iu', 'prefix' => 'il', 'linktbl' => 'imagelinks' ) @@ -67,7 +72,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ); $this->bl_code = $code; - $this->hasNS = $moduleName !== 'imagelinks'; + $this->hasNS = $moduleName !== 'imageusage'; if ($this->hasNS) { $this->bl_title = $prefix . '_title'; $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}"; @@ -93,13 +98,13 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } private function run($resultPageSet = null) { - $continue = $namespace = $redirect = $limit = null; - extract($this->extractRequestParams()); - + $this->params = $this->extractRequestParams(); + + $redirect = $this->params['redirect']; if ($redirect) - ApiBase :: dieDebug(__METHOD__, 'Redirect is not yet been implemented', 'notimplemented'); + $this->dieDebug('Redirect has not been implemented', 'notimplemented'); - $this->processContinue($continue, $redirect); + $this->processContinue(); $this->addFields($this->bl_fields); if (is_null($resultPageSet)) @@ -117,15 +122,19 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ($this->hasNS) $this->addWhereFld($this->bl_ns, $this->rootTitle->getNamespace()); $this->addWhereFld($this->bl_title, $this->rootTitle->getDBkey()); - $this->addWhereFld('page_namespace', $namespace); - $this->addOption('LIMIT', $limit +1); - $this->addOption('ORDER BY', $this->bl_sort); + $this->addWhereFld('page_namespace', $this->params['namespace']); - if ($redirect) + if($this->params['filterredir'] == 'redirects') + $this->addWhereFld('page_is_redirect', 1); + if($this->params['filterredir'] == 'nonredirects') $this->addWhereFld('page_is_redirect', 0); + $limit = $this->params['limit']; + $this->addOption('LIMIT', $limit +1); + $this->addOption('ORDER BY', $this->bl_sort); + $db = $this->getDB(); - if (!is_null($continue)) { + if (!is_null($this->params['continue'])) { $plfrm = intval($this->contID); if ($this->contLevel == 0) { // For the first level, there is only one target title, so no need for complex filtering @@ -150,48 +159,61 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... if ($redirect) { - $ns = $row-> { - $this->bl_ns }; - $t = $row-> { - $this->bl_title }; + $ns = $row-> { $this->bl_ns }; + $t = $row-> { $this->bl_title }; $continue = $this->getContinueRedirStr(false, 0, $ns, $t, $row->page_id); } else $continue = $this->getContinueStr($row->page_id); + // TODO: Security issue - if the user has no right to view next title, it will still be shown $this->setContinueEnumParameter('continue', $continue); break; } if (is_null($resultPageSet)) { - $vals = $this->addRowInfo('page', $row); + $vals = $this->extractRowInfo($row); if ($vals) - $data[intval($row->page_id)] = $vals; + $data[] = $vals; } else { $resultPageSet->processDbRow($row); } } $db->freeResult($res); - if (is_null($resultPageSet)) { + if (is_null($resultPageSet) && !empty($data)) { $result = $this->getResult(); $result->setIndexedTagName($data, $this->bl_code); $result->addValue('query', $this->getModuleName(), $data); } } - protected function processContinue($continue, $redirect) { + private function extractRowInfo($row) { + + $vals = array(); + $vals['pageid'] = intval($row->page_id); + ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->page_namespace, $row->page_title)); + + return $vals; + } + + protected function processContinue() { $pageSet = $this->getPageSet(); $count = $pageSet->getTitleCount(); - if (!is_null($continue)) { - if ($count !== 0) - $this->dieUsage("When continuing the {$this->getModuleName()} query, no other titles may be provided", 'titles_on_continue'); - $this->parseContinueParam($continue, $redirect); + + if (!is_null($this->params['continue'])) { + $this->parseContinueParam(); // Skip all completed links } else { - if ($count !== 1) - $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count'); - $this->rootTitle = current($pageSet->getTitles()); // only one title there + $title = $this->params['title']; + if (!is_null($title)) { + $this->rootTitle = Title :: newFromText($title); + } else { // This case is obsolete. Will support this for a while + if ($count !== 1) + $this->dieUsage("The {$this->getModuleName()} query requires one title to start", 'bad_title_count'); + $this->rootTitle = current($pageSet->getTitles()); // only one title there + $this->setWarning('Using titles parameter is obsolete for this list. Use ' . $this->encodeParamName('title') . ' instead.'); + } } // only image titles are allowed for the root @@ -199,9 +221,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title'); } - protected function parseContinueParam($continue, $redirect) { - $continueList = explode('|', $continue); - if ($redirect) { + protected function parseContinueParam() { + $continueList = explode('|', $this->params['continue']); + if ($this->params['redirect']) { // // expected redirect-mode parameter: // ns|db_key|step|level|ns|db_key|id @@ -215,7 +237,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $rootNs = intval($continueList[0]); if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) { $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]); - if ($this->rootTitle && $this->rootTitle->userCanRead()) { + if ($this->rootTitle) { $step = intval($continueList[2]); if ($step === 1 || $step === 2) { @@ -263,7 +285,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $rootNs = intval($continueList[0]); if (($rootNs !== 0 || $continueList[0] === '0') && !empty ($continueList[1])) { $this->rootTitle = Title :: makeTitleSafe($rootNs, $continueList[1]); - if ($this->rootTitle && $this->rootTitle->userCanRead()) { + if ($this->rootTitle) { $contID = intval($continueList[2]); if ($contID !== 0) { @@ -296,17 +318,26 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { protected function getAllowedParams() { return array ( + 'title' => null, 'continue' => null, 'namespace' => array ( ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => 'namespace' ), + 'filterredir' => array( + ApiBase :: PARAM_DFLT => 'all', + ApiBase :: PARAM_TYPE => array( + 'all', + 'redirects', + 'nonredirects' + ) + ), 'redirect' => false, 'limit' => array ( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ) ); @@ -314,8 +345,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { protected function getParamDescription() { return array ( + 'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.', 'continue' => 'When more results are available, use this to continue.', 'namespace' => 'The namespace to enumerate.', + 'filterredir' => 'How to filter for redirects', 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect (not implemented)', 'limit' => 'How many total pages to return.' ); @@ -327,7 +360,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { return 'Find all pages that link to the given page'; case 'embeddedin' : return 'Find all pages that embed (transclude) the given title'; - case 'imagelinks' : + case 'imageusage' : return 'Find all pages that use the given image title.'; default : ApiBase :: dieDebug(__METHOD__, 'Unknown module name'); @@ -337,16 +370,16 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { protected function getExamples() { static $examples = array ( 'backlinks' => array ( - "api.php?action=query&list=backlinks&titles=Main%20Page", - "api.php?action=query&generator=backlinks&titles=Main%20Page&prop=info" + "api.php?action=query&list=backlinks&bltitle=Main%20Page", + "api.php?action=query&generator=backlinks&gbltitle=Main%20Page&prop=info" ), 'embeddedin' => array ( - "api.php?action=query&list=embeddedin&titles=Template:Stub", - "api.php?action=query&generator=embeddedin&titles=Template:Stub&prop=info" + "api.php?action=query&list=embeddedin&eititle=Template:Stub", + "api.php?action=query&generator=embeddedin&geititle=Template:Stub&prop=info" ), - 'imagelinks' => array ( - "api.php?action=query&list=imagelinks&titles=Image:Albert%20Einstein%20Head.jpg", - "api.php?action=query&generator=imagelinks&titles=Image:Albert%20Einstein%20Head.jpg&prop=info" + 'imageusage' => array ( + "api.php?action=query&list=imageusage&iutitle=Image:Albert%20Einstein%20Head.jpg", + "api.php?action=query&generator=imageusage&giutitle=Image:Albert%20Einstein%20Head.jpg&prop=info" ) ); @@ -354,7 +387,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBacklinks.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryBacklinks.php 25476 2007-09-04 14:44:46Z catrope $'; } } -?> + diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index da07bb6c..28adb415 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,15 +29,19 @@ if (!defined('MEDIAWIKI')) { } /** + * This is a base class for all Query modules. + * It provides some common functionality such as constructing various SQL queries. + * * @addtogroup API */ abstract class ApiQueryBase extends ApiBase { - private $mQueryModule, $tables, $where, $fields, $options; + private $mQueryModule, $mDb, $tables, $where, $fields, $options; public function __construct($query, $moduleName, $paramPrefix = '') { parent :: __construct($query->getMain(), $moduleName, $paramPrefix); $this->mQueryModule = $query; + $this->mDb = null; $this->resetQueryParams(); } @@ -48,11 +52,16 @@ abstract class ApiQueryBase extends ApiBase { $this->options = array (); } - protected function addTables($value) { - if (is_array($value)) - $this->tables = array_merge($this->tables, $value); - else - $this->tables[] = $value; + protected function addTables($tables, $alias = null) { + if (is_array($tables)) { + if (!is_null($alias)) + ApiBase :: dieDebug(__METHOD__, 'Multiple table aliases not supported'); + $this->tables = array_merge($this->tables, $tables); + } else { + if (!is_null($alias)) + $tables = $this->getDB()->tableName($tables) . ' ' . $alias; + $this->tables[] = $tables; + } } protected function addFields($value) { @@ -124,176 +133,16 @@ abstract class ApiQueryBase extends ApiBase { return $res; } - protected function addRowInfo($prefix, $row) { - - $vals = array (); - - // ID - if ( isset( $row-> { $prefix . '_id' } ) ) - $vals[$prefix . 'id'] = intval( $row-> { $prefix . '_id' } ); - - // Title - $title = ApiQueryBase :: addRowInfo_title($row, $prefix . '_namespace', $prefix . '_title'); - if ($title) { - if (!$title->userCanRead()) - return false; - $vals['ns'] = $title->getNamespace(); - $vals['title'] = $title->getPrefixedText(); - } - - switch ($prefix) { - - case 'page' : - // page_is_redirect - @ $tmp = $row->page_is_redirect; - if ($tmp) - $vals['redirect'] = ''; - - break; - - case 'rc' : - // PageId - @ $tmp = $row->rc_cur_id; - if (!is_null($tmp)) - $vals['pageid'] = intval($tmp); - - @ $tmp = $row->rc_this_oldid; - if (!is_null($tmp)) - $vals['revid'] = intval($tmp); - - if ( isset( $row->rc_last_oldid ) ) - $vals['old_revid'] = intval( $row->rc_last_oldid ); - - $title = ApiQueryBase :: addRowInfo_title($row, 'rc_moved_to_ns', 'rc_moved_to_title'); - if ($title) { - if (!$title->userCanRead()) - return false; - $vals['new_ns'] = $title->getNamespace(); - $vals['new_title'] = $title->getPrefixedText(); - } - - if ( isset( $row->rc_patrolled ) ) - $vals['patrolled'] = ''; - - break; - - case 'log' : - // PageId - @ $tmp = $row->page_id; - if (!is_null($tmp)) - $vals['pageid'] = intval($tmp); - - if ($row->log_params !== '') { - $params = explode("\n", $row->log_params); - if ($row->log_type == 'move' && isset ($params[0])) { - $newTitle = Title :: newFromText($params[0]); - if ($newTitle) { - $vals['new_ns'] = $newTitle->getNamespace(); - $vals['new_title'] = $newTitle->getPrefixedText(); - $params = null; - } - } - - if (!empty ($params)) { - $this->getResult()->setIndexedTagName($params, 'param'); - $vals = array_merge($vals, $params); - } - } - - break; - - case 'rev' : - // PageID - @ $tmp = $row->rev_page; - if (!is_null($tmp)) - $vals['pageid'] = intval($tmp); - } - - // Type - @ $tmp = $row-> { - $prefix . '_type' }; - if (!is_null($tmp)) - $vals['type'] = $tmp; - - // Action - @ $tmp = $row-> { - $prefix . '_action' }; - if (!is_null($tmp)) - $vals['action'] = $tmp; - - // Old ID - @ $tmp = $row-> { - $prefix . '_text_id' }; - if (!is_null($tmp)) - $vals['oldid'] = intval($tmp); - - // User Name / Anon IP - @ $tmp = $row-> { - $prefix . '_user_text' }; - if (is_null($tmp)) - @ $tmp = $row->user_name; - if (!is_null($tmp)) { - $vals['user'] = $tmp; - @ $tmp = !$row-> { - $prefix . '_user' }; - if (!is_null($tmp) && $tmp) - $vals['anon'] = ''; - } - - // Bot Edit - @ $tmp = $row-> { - $prefix . '_bot' }; - if (!is_null($tmp) && $tmp) - $vals['bot'] = ''; - - // New Edit - @ $tmp = $row-> { - $prefix . '_new' }; - if (is_null($tmp)) - @ $tmp = $row-> { - $prefix . '_is_new' }; - if (!is_null($tmp) && $tmp) - $vals['new'] = ''; - - // Minor Edit - @ $tmp = $row-> { - $prefix . '_minor_edit' }; - if (is_null($tmp)) - @ $tmp = $row-> { - $prefix . '_minor' }; - if (!is_null($tmp) && $tmp) - $vals['minor'] = ''; - - // Timestamp - @ $tmp = $row-> { - $prefix . '_timestamp' }; - if (!is_null($tmp)) - $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $tmp); - - // Comment - @ $tmp = $row-> { - $prefix . '_comment' }; - if (!empty ($tmp)) // optimize bandwidth - $vals['comment'] = $tmp; - - return $vals; - } - - private static function addRowInfo_title($row, $nsfld, $titlefld) { - if ( isset( $row-> $nsfld ) ) { - $ns = $row-> $nsfld; - @ $title = $row-> $titlefld; - if (!empty ($title)) - return Title :: makeTitle($ns, $title); - } - return false; + public static function addTitleInfo(&$arr, $title, $prefix='') { + $arr[$prefix . 'ns'] = intval($title->getNamespace()); + $arr[$prefix . 'title'] = $title->getPrefixedText(); } - + /** * Override this method to request extra fields from the pageSet - * using $this->getPageSet()->requestField('fieldName') + * using $pageSet->requestField('fieldName') */ - public function requestExtraData() { + public function requestExtraData($pageSet) { } /** @@ -303,10 +152,25 @@ abstract class ApiQueryBase extends ApiBase { return $this->mQueryModule; } + /** + * Add sub-element under the page element with the given pageId. + */ + protected function addPageSubItems($pageId, $data) { + $result = $this->getResult(); + $result->setIndexedTagName($data, $this->getModulePrefix()); + $result->addValue(array ('query', 'pages', intval($pageId)), + $this->getModuleName(), + $data); + } + protected function setContinueEnumParameter($paramName, $paramValue) { - $msg = array ( - $this->encodeParamName($paramName - ) => $paramValue); + + $paramName = $this->encodeParamName($paramName); + $msg = array( $paramName => $paramValue ); + +// This is an alternative continue format as a part of the URL string +// ApiResult :: setContent($msg, $paramName . '=' . urlencode($paramValue)); + $this->getResult()->addValue('query-continue', $this->getModuleName(), $msg); } @@ -314,7 +178,19 @@ abstract class ApiQueryBase extends ApiBase { * Get the Query database connection (readonly) */ protected function getDB() { - return $this->getQuery()->getDB(); + if (is_null($this->mDb)) + $this->mDb = $this->getQuery()->getDB(); + return $this->mDb; + } + + /** + * Selects the query database connection with the given name. + * If no such connection has been requested before, it will be created. + * Subsequent calls with the same $name will return the same connection + * as the first, regardless of $db or $groups new values. + */ + public function selectNamedDB($name, $db, $groups) { + $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups); } /** @@ -322,7 +198,7 @@ abstract class ApiQueryBase extends ApiBase { * @return ApiPageSet data */ protected function getPageSet() { - return $this->mQueryModule->getPageSet(); + return $this->getQuery()->getPageSet(); } /** @@ -338,8 +214,19 @@ abstract class ApiQueryBase extends ApiBase { return str_replace('_', ' ', $key); } + public function getTokenFlag($tokenArr, $action) { + if (in_array($action, $tokenArr)) { + global $wgUser; + if ($wgUser->isAllowed($action)) + return true; + else + $this->dieUsage("Action '$action' is not allowed for the current user", 'permissiondenied'); + } + return false; + } + public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiQueryBase.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryBase.php 24533 2007-08-01 22:46:22Z yurik $'; } } @@ -375,4 +262,4 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { */ public abstract function executeGenerator($resultPageSet); } -?> + diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php new file mode 100644 index 00000000..42bc1c38 --- /dev/null +++ b/includes/api/ApiQueryCategories.php @@ -0,0 +1,157 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * A query module to enumerate categories the set of pages belong to. + * + * @addtogroup API + */ +class ApiQueryCategories extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'cl'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + if ($this->getPageSet()->getGoodTitleCount() == 0) + return; // nothing to do + + $params = $this->extractRequestParams(); + $prop = $params['prop']; + + $this->addFields(array ( + 'cl_from', + 'cl_to' + )); + + $fld_sortkey = false; + if (!is_null($prop)) { + foreach($prop as $p) { + switch ($p) { + case 'sortkey': + $this->addFields('cl_sortkey'); + $fld_sortkey = true; + break; + default : + ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); + } + } + } + + $this->addTables('categorylinks'); + $this->addWhereFld('cl_from', array_keys($this->getPageSet()->getGoodTitles())); + $this->addOption('ORDER BY', "cl_from, cl_to"); + + $db = $this->getDB(); + $res = $this->select(__METHOD__); + + if (is_null($resultPageSet)) { + + $data = array(); + $lastId = 0; // database has no ID 0 + while ($row = $db->fetchObject($res)) { + if ($lastId != $row->cl_from) { + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + $data = array(); + } + $lastId = $row->cl_from; + } + + $title = Title :: makeTitle(NS_CATEGORY, $row->cl_to); + + $vals = array(); + ApiQueryBase :: addTitleInfo($vals, $title); + if ($fld_sortkey) + $vals['sortkey'] = $row->cl_sortkey; + + $data[] = $vals; + } + + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + } + + } else { + + $titles = array(); + while ($row = $db->fetchObject($res)) { + $titles[] = Title :: makeTitle(NS_CATEGORY, $row->cl_to); + } + $resultPageSet->populateFromTitles($titles); + } + + $db->freeResult($res); + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'sortkey', + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'Which additional properties to get for each category.', + ); + } + + protected function getDescription() { + return 'List all categories the page(s) belong to'; + } + + protected function getExamples() { + return array ( + "Get a list of categories [[Albert Einstein]] belongs to:", + " api.php?action=query&prop=categories&titles=Albert%20Einstein", + "Get information about all categories used in the [[Albert Einstein]]:", + " api.php?action=query&generator=categories&titles=Albert%20Einstein&prop=info" + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryCategories.php 24092 2007-07-14 19:04:31Z yurik $'; + } +} + diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php new file mode 100644 index 00000000..58a454a5 --- /dev/null +++ b/includes/api/ApiQueryCategoryMembers.php @@ -0,0 +1,238 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * A query module to enumerate pages that belong to a category. + * + * @addtogroup API + */ +class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'cm'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + $params = $this->extractRequestParams(); + + $category = $params['category']; + if (is_null($category)) + $this->dieUsage("Category parameter is required", 'param_category'); + $categoryTitle = Title::makeTitleSafe( NS_CATEGORY, $category ); + if ( is_null( $categoryTitle ) ) + $this->dieUsage("Category name $category is not valid", 'param_category'); + + $prop = array_flip($params['prop']); + $fld_ids = isset($prop['ids']); + $fld_title = isset($prop['title']); + $fld_sortkey = isset($prop['sortkey']); + $fld_timestamp = isset($prop['timestamp']); + + if (is_null($resultPageSet)) { + $this->addFields(array('cl_from', 'cl_sortkey', 'page_namespace', 'page_title')); + $this->addFieldsIf('page_id', $fld_ids); + } else { + $this->addFields($resultPageSet->getPageTableFields()); // will include page_ id, ns, title + $this->addFields(array('cl_from', 'cl_sortkey')); + } + + $this->addFieldsIf('cl_timestamp', $fld_timestamp); + $this->addTables(array('page','categorylinks')); // must be in this order for 'USE INDEX' + // Not needed after bug 10280 is applied to servers + if($params['sort'] == 'timestamp') + { + $this->addOption('USE INDEX', 'cl_timestamp'); + $this->addOption('ORDER BY', 'cl_to, cl_timestamp'); + } + else + { + $this->addOption('USE INDEX', 'cl_sortkey'); + $this->addOption('ORDER BY', 'cl_to, cl_sortkey, cl_from'); + } + + $this->addWhere('cl_from=page_id'); + $this->setContinuation($params['continue']); + $this->addWhereFld('cl_to', $categoryTitle->getDBkey()); + $this->addWhereFld('page_namespace', $params['namespace']); + + $limit = $params['limit']; + $this->addOption('LIMIT', $limit +1); + + $db = $this->getDB(); + + $data = array (); + $count = 0; + $lastSortKey = null; + $res = $this->select(__METHOD__); + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // TODO: Security issue - if the user has no right to view next title, it will still be shown + $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey)); + break; + } + + $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys + + if (is_null($resultPageSet)) { + $vals = array(); + if ($fld_ids) + $vals['pageid'] = intval($row->page_id); + if ($fld_title) { + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + $vals['ns'] = intval($title->getNamespace()); + $vals['title'] = $title->getPrefixedText(); + } + if ($fld_sortkey) + $vals['sortkey'] = $row->cl_sortkey; + if ($fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp); + $data[] = $vals; + } else { + $resultPageSet->processDbRow($row); + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $this->getResult()->setIndexedTagName($data, 'cm'); + $this->getResult()->addValue('query', $this->getModuleName(), $data); + } + } + + private function getContinueStr($row, $lastSortKey) { + $ret = $row->cl_sortkey . '|'; + if ($row->cl_sortkey == $lastSortKey) // duplicate sort key, add cl_from + $ret .= $row->cl_from; + return $ret; + } + + /** + * Add DB WHERE clause to continue previous query based on 'continue' parameter + */ + private function setContinuation($continue) { + if (is_null($continue)) + return; // This is not a continuation request + + $continueList = explode('|', $continue); + $hasError = count($continueList) != 2; + $from = 0; + if (!$hasError && strlen($continueList[1]) > 0) { + $from = intval($continueList[1]); + $hasError = ($from == 0); + } + + if ($hasError) + $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue"); + + $encSortKey = $this->getDB()->addQuotes($continueList[0]); + $encFrom = $this->getDB()->addQuotes($from); + + if ($from != 0) { + // Duplicate sort key continue + $this->addWhere( "cl_sortkey>$encSortKey OR (cl_sortkey=$encSortKey AND cl_from>=$encFrom)" ); + } else { + $this->addWhere( "cl_sortkey>=$encSortKey" ); + } + } + + protected function getAllowedParams() { + return array ( + 'category' => null, + 'prop' => array ( + ApiBase :: PARAM_DFLT => 'ids|title', + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'ids', + 'title', + 'sortkey', + 'timestamp', + ) + ), + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace', + ), + 'continue' => null, + 'limit' => array ( + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'sort' => array( + ApiBase :: PARAM_DFLT => 'sortkey', + ApiBase :: PARAM_TYPE => array( + 'sortkey', + 'timestamp' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'category' => 'Which category to enumerate (required)', + 'prop' => 'What pieces of information to include', + 'namespace' => 'Only include pages in these namespaces', + 'sort' => 'Property to sort by', + 'continue' => 'For large categories, give the value retured from previous query', + 'limit' => 'The maximum number of pages to return.', + ); + } + + protected function getDescription() { + return 'List all pages in a given category'; + } + + protected function getExamples() { + return array ( + "Get first 10 pages in the categories [[Physics]]:", + " api.php?action=query&list=categorymembers&cmcategory=Physics", + "Get page info about first 10 pages in the categories [[Physics]]:", + " api.php?action=query&generator=categorymembers&gcmcategory=Physics&prop=info", + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 25474 2007-09-04 14:30:31Z catrope $'; + } +} + diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php new file mode 100644 index 00000000..385ae65b --- /dev/null +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -0,0 +1,200 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * @addtogroup API + */ +class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'eu'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + $params = $this->extractRequestParams(); + + $protocol = $params['protocol']; + $query = $params['query']; + if (is_null($query)) + $this->dieUsage('Missing required query parameter', 'params'); + + // Find the right prefix + global $wgUrlProtocols; + foreach ($wgUrlProtocols as $p) { + if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) { + $protocol = $p; + break; + } + } + + $likeQuery = LinkFilter::makeLike($query , $protocol); + if (!$likeQuery) + $this->dieUsage('Invalid query', 'bad_query'); + $likeQuery = substr($likeQuery, 0, strpos($likeQuery,'%')+1); + + $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX' + $this->addOption('USE INDEX', 'el_index'); + + $db = $this->getDB(); + $this->addWhere('page_id=el_from'); + $this->addWhere('el_index LIKE ' . $db->addQuotes( $likeQuery )); + $this->addWhereFld('page_namespace', $params['namespace']); + + $prop = array_flip($params['prop']); + $fld_ids = isset($prop['ids']); + $fld_title = isset($prop['title']); + $fld_url = isset($prop['url']); + + if (is_null($resultPageSet)) { + $this->addFields(array ( + 'page_id', + 'page_namespace', + 'page_title' + )); + $this->addFieldsIf('el_to', $fld_url); + } else { + $this->addFields($resultPageSet->getPageTableFields()); + } + + $limit = $params['limit']; + $offset = $params['offset']; + $this->addOption('LIMIT', $limit +1); + if (isset ($offset)) + $this->addOption('OFFSET', $offset); + + $res = $this->select(__METHOD__); + + $data = array (); + $count = 0; + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('offset', $offset+$limit+1); + break; + } + + if (is_null($resultPageSet)) { + $vals = array(); + if ($fld_ids) + $vals['pageid'] = intval($row->page_id); + if ($fld_title) { + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + $vals['ns'] = intval($title->getNamespace()); + $vals['title'] = $title->getPrefixedText(); + } + if ($fld_url) + $vals['url'] = $row->el_to; + $data[] = $vals; + } else { + $resultPageSet->processDbRow($row); + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($data, $this->getModulePrefix()); + $result->addValue('query', $this->getModuleName(), $data); + } + } + + protected function getAllowedParams() { + global $wgUrlProtocols; + $protocols = array(); + foreach ($wgUrlProtocols as $p) { + $protocols[] = substr($p, 0, strpos($p,':')); + } + + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'ids|title|url', + ApiBase :: PARAM_TYPE => array ( + 'ids', + 'title', + 'url' + ) + ), + 'offset' => array ( + ApiBase :: PARAM_TYPE => 'integer' + ), + 'protocol' => array ( + ApiBase :: PARAM_TYPE => $protocols, + ApiBase :: PARAM_DFLT => 'http', + ), + 'query' => null, + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'What pieces of information to include', + 'offset' => 'Used for paging. Use the value returned for "continue"', + 'protocol' => 'Protocol of the url', + 'query' => 'Search string without protocol. See [[Special:LinkSearch]]', + 'namespace' => 'The page namespace(s) to enumerate.', + 'limit' => 'How many entries to return.' + ); + } + + protected function getDescription() { + return 'Enumerate pages that contain a given URL'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=exturlusage&euquery=www.mediawiki.org' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 24694 2007-08-09 08:41:58Z yurik $'; + } +} diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php new file mode 100644 index 00000000..440b31d6 --- /dev/null +++ b/includes/api/ApiQueryExternalLinks.php @@ -0,0 +1,93 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * A query module to list all external URLs found on a given set of pages. + * + * @addtogroup API + */ +class ApiQueryExternalLinks extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'el'); + } + + public function execute() { + + $this->addFields(array ( + 'el_from', + 'el_to' + )); + + $this->addTables('externallinks'); + $this->addWhereFld('el_from', array_keys($this->getPageSet()->getGoodTitles())); + + $db = $this->getDB(); + $res = $this->select(__METHOD__); + + $data = array(); + $lastId = 0; // database has no ID 0 + while ($row = $db->fetchObject($res)) { + if ($lastId != $row->el_from) { + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + $data = array(); + } + $lastId = $row->el_from; + } + + $entry = array(); + ApiResult :: setContent($entry, $row->el_to); + $data[] = $entry; + } + + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + } + + $db->freeResult($res); + } + + protected function getDescription() { + return 'Returns all external urls (not interwikies) from the given page(s)'; + } + + protected function getExamples() { + return array ( + "Get a list of external links on the [[Main Page]]:", + " api.php?action=query&prop=extlinks&titles=Main%20Page", + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 23819 2007-07-07 03:05:09Z yurik $'; + } +} + diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php new file mode 100644 index 00000000..3d568ba1 --- /dev/null +++ b/includes/api/ApiQueryImageInfo.php @@ -0,0 +1,156 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * A query action to get image information and upload history. + * + * @addtogroup API + */ +class ApiQueryImageInfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'ii'); + } + + public function execute() { + $params = $this->extractRequestParams(); + + $history = $params['history']; + + $prop = array_flip($params['prop']); + $fld_timestamp = isset($prop['timestamp']); + $fld_user = isset($prop['user']); + $fld_comment = isset($prop['comment']); + $fld_url = isset($prop['url']); + $fld_size = isset($prop['size']); + $fld_sha1 = isset($prop['sha1']); + + $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); + if (!empty($pageIds[NS_IMAGE])) { + foreach ($pageIds[NS_IMAGE] as $dbKey => $pageId) { + + $title = Title :: makeTitle(NS_IMAGE, $dbKey); + $img = wfFindFile($title); + + $data = array(); + if ( !$img ) { + $repository = ''; + } else { + + $repository = $img->getRepoName(); + + $isCur = true; + while($line = $img->nextHistoryLine()) { // assignment + $row = get_object_vars( $line ); + $vals = array(); + $prefix = $isCur ? 'img' : 'oi'; + + if ($fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row["${prefix}_timestamp"]); + if ($fld_user) { + $vals['user'] = $row["${prefix}_user_text"]; + if(!$row["${prefix}_user"]) + $vals['anon'] = ''; + } + if ($fld_size) { + $vals['size'] = intval($row["{$prefix}_size"]); + $vals['width'] = intval($row["{$prefix}_width"]); + $vals['height'] = intval($row["{$prefix}_height"]); + } + if ($fld_url) + $vals['url'] = $isCur ? $img->getURL() : $img->getArchiveUrl($row["oi_archive_name"]); + if ($fld_comment) + $vals['comment'] = $row["{$prefix}_description"]; + + if ($fld_sha1) + $vals['sha1'] = wfBaseConvert($row["{$prefix}_sha1"], 36, 16, 40); + + $data[] = $vals; + + if (!$history) // Stop after the first line. + break; + + $isCur = false; + } + + $img->resetHistory(); + } + + $this->getResult()->addValue(array ('query', 'pages', intval($pageId)), + 'imagerepository', + $repository); + if (!empty($data)) + $this->addPageSubItems($pageId, $data); + } + } + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'timestamp|user', + ApiBase :: PARAM_TYPE => array ( + 'timestamp', + 'user', + 'comment', + 'url', + 'size', + 'sha1' + ) + ), + 'history' => false, + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'What image information to get.', + 'history' => 'Include upload history', + ); + } + + protected function getDescription() { + return array ( + 'Returns image information and upload history' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=query&titles=Image:Albert%20Einstein%20Head.jpg&prop=imageinfo', + 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iihistory&iiprop=timestamp|user|url', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryImageInfo.php 25456 2007-09-03 19:58:05Z catrope $'; + } +} diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php new file mode 100644 index 00000000..d64a653b --- /dev/null +++ b/includes/api/ApiQueryImages.php @@ -0,0 +1,118 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * This query adds subelement to all pages with the list of images embedded into those pages. + * + * @addtogroup API + */ +class ApiQueryImages extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'im'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + if ($this->getPageSet()->getGoodTitleCount() == 0) + return; // nothing to do + + $this->addFields(array ( + 'il_from', + 'il_to' + )); + + $this->addTables('imagelinks'); + $this->addWhereFld('il_from', array_keys($this->getPageSet()->getGoodTitles())); + $this->addOption('ORDER BY', "il_from, il_to"); + + $db = $this->getDB(); + $res = $this->select(__METHOD__); + + if (is_null($resultPageSet)) { + + $data = array(); + $lastId = 0; // database has no ID 0 + while ($row = $db->fetchObject($res)) { + if ($lastId != $row->il_from) { + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + $data = array(); + } + $lastId = $row->il_from; + } + + $vals = array(); + ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_IMAGE, $row->il_to)); + $data[] = $vals; + } + + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + } + + } else { + + $titles = array(); + while ($row = $db->fetchObject($res)) { + $titles[] = Title :: makeTitle(NS_IMAGE, $row->il_to); + } + $resultPageSet->populateFromTitles($titles); + } + + $db->freeResult($res); + } + + protected function getDescription() { + return 'Returns all images contained on the given page(s)'; + } + + protected function getExamples() { + return array ( + "Get a list of images used in the [[Main Page]]:", + " api.php?action=query&prop=images&titles=Main%20Page", + "Get information about all images used in the [[Main Page]]:", + " api.php?action=query&generator=images&titles=Main%20Page&prop=info" + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryImages.php 24092 2007-07-14 19:04:31Z yurik $'; + } +} + diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 77489a5f..bebf4006 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,59 +29,167 @@ if (!defined('MEDIAWIKI')) { } /** + * A query module to show basic page information. + * * @addtogroup API */ class ApiQueryInfo extends ApiQueryBase { public function __construct($query, $moduleName) { - parent :: __construct($query, $moduleName); + parent :: __construct($query, $moduleName, 'in'); } - public function requestExtraData() { - $pageSet = $this->getPageSet(); + public function requestExtraData($pageSet) { $pageSet->requestField('page_is_redirect'); + $pageSet->requestField('page_is_new'); + $pageSet->requestField('page_counter'); $pageSet->requestField('page_touched'); $pageSet->requestField('page_latest'); + $pageSet->requestField('page_len'); } public function execute() { + global $wgUser; + + $params = $this->extractRequestParams(); + $fld_protection = false; + if(!is_null($params['prop'])) { + $prop = array_flip($params['prop']); + $fld_protection = isset($prop['protection']); + } + if(!is_null($params['token'])) { + $token = $params['token']; + $tok_edit = $this->getTokenFlag($token, 'edit'); + $tok_delete = $this->getTokenFlag($token, 'delete'); + $tok_protect = $this->getTokenFlag($token, 'protect'); + $tok_move = $this->getTokenFlag($token, 'move'); + } + $pageSet = $this->getPageSet(); $titles = $pageSet->getGoodTitles(); $result = $this->getResult(); $pageIsRedir = $pageSet->getCustomField('page_is_redirect'); + $pageIsNew = $pageSet->getCustomField('page_is_new'); + $pageCounter = $pageSet->getCustomField('page_counter'); $pageTouched = $pageSet->getCustomField('page_touched'); $pageLatest = $pageSet->getCustomField('page_latest'); + $pageLength = $pageSet->getCustomField('page_len'); + + if ($fld_protection && count($titles) > 0) { + $this->addTables('page_restrictions'); + $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry')); + $this->addWhereFld('pr_page', array_keys($titles)); - foreach ( $titles as $pageid => $unused ) { + $db = $this->getDB(); + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) { + $protections[$row->pr_page][] = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ) + ); + } + $db->freeResult($res); + } + + foreach ( $titles as $pageid => $title ) { $pageInfo = array ( 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]), - 'lastrevid' => intval($pageLatest[$pageid]) + 'lastrevid' => intval($pageLatest[$pageid]), + 'counter' => intval($pageCounter[$pageid]), + 'length' => intval($pageLength[$pageid]), ); if ($pageIsRedir[$pageid]) $pageInfo['redirect'] = ''; + if ($pageIsNew[$pageid]) + $pageInfo['new'] = ''; + + if (!is_null($token)) { + // Currently all tokens are generated the same way, but it might change + if ($tok_edit) + $pageInfo['edittoken'] = $wgUser->editToken(); + if ($tok_delete) + $pageInfo['deletetoken'] = $wgUser->editToken(); + if ($tok_protect) + $pageInfo['protecttoken'] = $wgUser->editToken(); + if ($tok_move) + $pageInfo['movetoken'] = $wgUser->editToken(); + } + + if($fld_protection) { + if (isset($protections[$pageid])) { + $pageInfo['protection'] = $protections[$pageid]; + $result->setIndexedTagName($pageInfo['protection'], 'pr'); + } else { + $pageInfo['protection'] = array(); + } + } + $result->addValue(array ( 'query', 'pages' ), $pageid, $pageInfo); } + + // Get edit tokens for missing titles if requested + // Delete, protect and move tokens are N/A for missing titles anyway + if($tok_edit) + { + $missing = $pageSet->getMissingTitles(); + $res = $result->getData(); + foreach($missing as $pageid => $title) + $res['query']['pages'][$pageid]['edittoken'] = $wgUser->editToken(); + } } + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_DFLT => NULL, + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'protection' + )), + 'token' => array ( + ApiBase :: PARAM_DFLT => NULL, + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'edit', + 'delete', + 'protect', + 'move', + )), + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => array ( + 'Which additional properties to get:', + ' "protection" - List the protection level of each page' + ), + 'token' => 'Request a token to perform a data-modifying action on a page', + ); + } + + protected function getDescription() { return 'Get basic page information such as namespace, title, last touched date, ...'; } protected function getExamples() { return array ( - 'api.php?action=query&prop=info&titles=Main%20Page' + 'api.php?action=query&prop=info&titles=Main%20Page', + 'api.php?action=query&prop=info&inprop=protection&titles=Main%20Page' ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryInfo.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryInfo.php 25457 2007-09-03 20:17:53Z catrope $'; } } -?> + diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php new file mode 100644 index 00000000..ae5ff790 --- /dev/null +++ b/includes/api/ApiQueryLangLinks.php @@ -0,0 +1,94 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * A query module to list all langlinks (links to correspanding foreign language pages). + * + * @addtogroup API + */ +class ApiQueryLangLinks extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'll'); + } + + public function execute() { + $this->addFields(array ( + 'll_from', + 'll_lang', + 'll_title' + )); + + $this->addTables('langlinks'); + $this->addWhereFld('ll_from', array_keys($this->getPageSet()->getGoodTitles())); + $this->addOption('ORDER BY', "ll_from, ll_lang"); + $res = $this->select(__METHOD__); + + $data = array(); + $lastId = 0; // database has no ID 0 + $db = $this->getDB(); + while ($row = $db->fetchObject($res)) { + + if ($lastId != $row->ll_from) { + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + $data = array(); + } + $lastId = $row->ll_from; + } + + $entry = array('lang'=>$row->ll_lang); + ApiResult :: setContent($entry, $row->ll_title); + $data[] = $entry; + } + + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + } + + $db->freeResult($res); + } + + protected function getDescription() { + return 'Returns all interlanguage links from the given page(s)'; + } + + protected function getExamples() { + return array ( + "Get interlanguage links from the [[Main Page]]:", + " api.php?action=query&prop=langlinks&titles=Main%20Page&redirects", + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryLangLinks.php 23819 2007-07-07 03:05:09Z yurik $'; + } +} + diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php new file mode 100644 index 00000000..7ec20f44 --- /dev/null +++ b/includes/api/ApiQueryLinks.php @@ -0,0 +1,162 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiQueryBase.php"); +} + +/** + * A query module to list all wiki links on a given set of pages. + * + * @addtogroup API + */ +class ApiQueryLinks extends ApiQueryGeneratorBase { + + const LINKS = 'links'; + const TEMPLATES = 'templates'; + + private $table, $prefix, $description; + + public function __construct($query, $moduleName) { + + switch ($moduleName) { + case self::LINKS : + $this->table = 'pagelinks'; + $this->prefix = 'pl'; + $this->description = 'link'; + break; + case self::TEMPLATES : + $this->table = 'templatelinks'; + $this->prefix = 'tl'; + $this->description = 'template'; + break; + default : + ApiBase :: dieDebug(__METHOD__, 'Unknown module name'); + } + + parent :: __construct($query, $moduleName, $this->prefix); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + if ($this->getPageSet()->getGoodTitleCount() == 0) + return; // nothing to do + + $params = $this->extractRequestParams(); + + $this->addFields(array ( + $this->prefix . '_from pl_from', + $this->prefix . '_namespace pl_namespace', + $this->prefix . '_title pl_title' + )); + + $this->addTables($this->table); + $this->addWhereFld($this->prefix . '_from', array_keys($this->getPageSet()->getGoodTitles())); + $this->addWhereFld($this->prefix . '_namespace', $params['namespace']); + $this->addOption('ORDER BY', str_replace('pl_', $this->prefix . '_', 'pl_from, pl_namespace, pl_title')); + + $db = $this->getDB(); + $res = $this->select(__METHOD__); + + if (is_null($resultPageSet)) { + + $data = array(); + $lastId = 0; // database has no ID 0 + while ($row = $db->fetchObject($res)) { + if ($lastId != $row->pl_from) { + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + $data = array(); + } + $lastId = $row->pl_from; + } + + $vals = array(); + ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->pl_namespace, $row->pl_title)); + $data[] = $vals; + } + + if($lastId != 0) { + $this->addPageSubItems($lastId, $data); + } + + } else { + + $titles = array(); + while ($row = $db->fetchObject($res)) { + $titles[] = Title :: makeTitle($row->pl_namespace, $row->pl_title); + } + $resultPageSet->populateFromTitles($titles); + } + + $db->freeResult($res); + } + + protected function getAllowedParams() + { + return array( + 'namespace' => array( + ApiBase :: PARAM_TYPE => 'namespace', + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + protected function getParamDescription() + { + return array( + 'namespace' => "Show {$this->description}s in this namespace(s) only" + ); + } + + protected function getDescription() { + return "Returns all {$this->description}s from the given page(s)"; + } + + protected function getExamples() { + return array ( + "Get {$this->description}s from the [[Main Page]]:", + " api.php?action=query&prop={$this->getModuleName()}&titles=Main%20Page", + "Get information about the {$this->description} pages in the [[Main Page]]:", + " api.php?action=query&generator={$this->getModuleName()}&titles=Main%20Page&prop=info", + "Get {$this->description}s from the Main Page in the User and Template namespaces:", + " api.php?action=query&prop={$this->getModuleName()}&titles=Main%20Page&{$this->prefix}namespace=2|10" + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryLinks.php 24092 2007-07-14 19:04:31Z yurik $'; + } +} + diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index d9f23758..0f143658 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * Query action to List the log events, with optional filtering by various parameters. + * * @addtogroup API */ class ApiQueryLogEvents extends ApiQueryBase { @@ -38,11 +40,18 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function execute() { - $limit = $type = $start = $end = $dir = $user = $title = null; - extract($this->extractRequestParams()); - + $params = $this->extractRequestParams(); $db = $this->getDB(); + $prop = $params['prop']; + $this->fld_ids = in_array('ids', $prop); + $this->fld_title = in_array('title', $prop); + $this->fld_type = in_array('type', $prop); + $this->fld_user = in_array('user', $prop); + $this->fld_timestamp = in_array('timestamp', $prop); + $this->fld_comment = in_array('comment', $prop); + $this->fld_details = in_array('details', $prop); + list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user'); $this->addOption('STRAIGHT_JOIN'); @@ -54,19 +63,27 @@ class ApiQueryLogEvents extends ApiQueryBase { 'log_type', 'log_action', 'log_timestamp', - 'log_user', - 'user_name', - 'log_namespace', - 'log_title', - 'page_id', - 'log_comment', - 'log_params' )); + + // FIXME: Fake out log_id for now until the column is live on Wikimedia + // $this->addFieldsIf('log_id', $this->fld_ids); + $this->addFieldsIf('page_id', $this->fld_ids); + $this->addFieldsIf('log_user', $this->fld_user); + $this->addFieldsIf('user_name', $this->fld_user); + $this->addFieldsIf('log_namespace', $this->fld_title); + $this->addFieldsIf('log_title', $this->fld_title); + $this->addFieldsIf('log_comment', $this->fld_comment); + $this->addFieldsIf('log_params', $this->fld_details); + + + $this->addWhereFld('log_deleted', 0); + $this->addWhereFld('log_type', $params['type']); + $this->addWhereRange('log_timestamp', $params['dir'], $params['start'], $params['end']); - $this->addWhereFld('log_type', $type); - $this->addWhereRange('log_timestamp', $dir, $start, $end); + $limit = $params['limit']; $this->addOption('LIMIT', $limit +1); + $user = $params['user']; if (!is_null($user)) { $userid = $db->selectField('user', 'user_id', array ( 'user_name' => $user @@ -76,6 +93,7 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addWhereFld('log_user', $userid); } + $title = $params['title']; if (!is_null($title)) { $titleObj = Title :: newFromText($title); if (is_null($titleObj)) @@ -90,11 +108,11 @@ class ApiQueryLogEvents extends ApiQueryBase { while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('start', $row->log_timestamp); + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->log_timestamp)); break; } - $vals = $this->addRowInfo('log', $row); + $vals = $this->extractRowInfo($row); if($vals) $data[] = $vals; } @@ -104,23 +122,102 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->getResult()->addValue('query', $this->getModuleName(), $data); } + private function extractRowInfo($row) { + $vals = array(); + + if ($this->fld_ids) { + // FIXME: Fake out log_id for now until the column is live on Wikimedia + // $vals['logid'] = intval($row->log_id); + $vals['logid'] = 0; + $vals['pageid'] = intval($row->page_id); + } + + if ($this->fld_title) { + $title = Title :: makeTitle($row->log_namespace, $row->log_title); + ApiQueryBase :: addTitleInfo($vals, $title); + } + + if ($this->fld_type) { + $vals['type'] = $row->log_type; + $vals['action'] = $row->log_action; + } + + if ($this->fld_details && $row->log_params !== '') { + $params = explode("\n", $row->log_params); + switch ($row->log_type) { + case 'move': + if (isset ($params[0])) { + $title = Title :: newFromText($params[0]); + if ($title) { + $vals2 = array(); + ApiQueryBase :: addTitleInfo($vals2, $title, "new_"); + $vals[$row->log_type] = $vals2; + $params = null; + } + } + break; + case 'patrol': + $vals2 = array(); + list( $vals2['cur'], $vals2['prev'], $vals2['auto'] ) = $params; + $vals[$row->log_type] = $vals2; + $params = null; + break; + case 'rights': + $vals2 = array(); + list( $vals2['old'], $vals2['new'] ) = $params; + $vals[$row->log_type] = $vals2; + $params = null; + break; + case 'block': + $vals2 = array(); + list( $vals2['duration'], $vals2['flags'] ) = $params; + $vals[$row->log_type] = $vals2; + $params = null; + break; + } + + if (isset($params)) { + $this->getResult()->setIndexedTagName($params, 'param'); + $vals = array_merge($vals, $params); + } + } + + if ($this->fld_user) { + $vals['user'] = $row->user_name; + if(!$row->log_user) + $vals['anon'] = ''; + } + if ($this->fld_timestamp) { + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->log_timestamp); + } + if ($this->fld_comment && !empty ($row->log_comment)) { + $vals['comment'] = $row->log_comment; + } + + return $vals; + } + + protected function getAllowedParams() { + global $wgLogTypes; return array ( - 'type' => array ( + 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'ids|title|type|user|timestamp|comment|details', ApiBase :: PARAM_TYPE => array ( - 'block', - 'protect', - 'rights', - 'delete', - 'upload', - 'move', - 'import', - 'renameuser', - 'newusers', - 'makebot' + 'ids', + 'title', + 'type', + 'user', + 'timestamp', + 'comment', + 'details', ) ), + 'type' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $wgLogTypes + ), 'start' => array ( ApiBase :: PARAM_TYPE => 'timestamp' ), @@ -140,7 +237,7 @@ class ApiQueryLogEvents extends ApiQueryBase { ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ) ); @@ -169,7 +266,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLogEvents.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryLogEvents.php 24256 2007-07-18 21:47:09Z robchurch $'; } } -?> + diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 25f7ff3e..309beaf9 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,9 @@ if (!defined('MEDIAWIKI')) { } /** + * A query action to enumerate the recent changes that were done to the wiki. + * Various filters are supported. + * * @addtogroup API */ class ApiQueryRecentChanges extends ApiQueryBase { @@ -37,6 +40,10 @@ class ApiQueryRecentChanges extends ApiQueryBase { parent :: __construct($query, $moduleName, 'rc'); } + private $fld_comment = false, $fld_user = false, $fld_flags = false, + $fld_timestamp = false, $fld_title = false, $fld_ids = false, + $fld_sizes = false; + public function execute() { $limit = $prop = $namespace = $show = $dir = $start = $end = null; extract($this->extractRequestParams()); @@ -44,6 +51,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addTables('recentchanges'); $this->addWhereRange('rc_timestamp', $dir, $start, $end); $this->addWhereFld('rc_namespace', $namespace); + $this->addWhereFld('rc_deleted', 0); if (!is_null($show)) { $show = array_flip($show); @@ -62,9 +70,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'rc_timestamp', 'rc_namespace', 'rc_title', - 'rc_cur_id', - 'rc_this_oldid', - 'rc_last_oldid', 'rc_type', 'rc_moved_to_ns', 'rc_moved_to_title' @@ -72,16 +77,27 @@ class ApiQueryRecentChanges extends ApiQueryBase { if (!is_null($prop)) { $prop = array_flip($prop); - $this->addFieldsIf('rc_comment', isset ($prop['comment'])); - if (isset ($prop['user'])) { - $this->addFields('rc_user'); - $this->addFields('rc_user_text'); - } - if (isset ($prop['flags'])) { - $this->addFields('rc_minor'); - $this->addFields('rc_bot'); - $this->addFields('rc_new'); - } + + $this->fld_comment = isset ($prop['comment']); + $this->fld_user = isset ($prop['user']); + $this->fld_flags = isset ($prop['flags']); + $this->fld_timestamp = isset ($prop['timestamp']); + $this->fld_title = isset ($prop['title']); + $this->fld_ids = isset ($prop['ids']); + $this->fld_sizes = isset ($prop['sizes']); + + $this->addFieldsIf('rc_id', $this->fld_ids); + $this->addFieldsIf('rc_cur_id', $this->fld_ids); + $this->addFieldsIf('rc_this_oldid', $this->fld_ids); + $this->addFieldsIf('rc_last_oldid', $this->fld_ids); + $this->addFieldsIf('rc_comment', $this->fld_comment); + $this->addFieldsIf('rc_user', $this->fld_user); + $this->addFieldsIf('rc_user_text', $this->fld_user); + $this->addFieldsIf('rc_minor', $this->fld_flags); + $this->addFieldsIf('rc_bot', $this->fld_flags); + $this->addFieldsIf('rc_new', $this->fld_flags); + $this->addFieldsIf('rc_old_len', $this->fld_sizes); + $this->addFieldsIf('rc_new_len', $this->fld_sizes); } $this->addOption('LIMIT', $limit +1); @@ -91,15 +107,16 @@ class ApiQueryRecentChanges extends ApiQueryBase { $count = 0; $db = $this->getDB(); $res = $this->select(__METHOD__); + while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('start', $row->rc_timestamp); + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp)); break; } - $vals = $this->addRowInfo('rc', $row); - if ($vals) + $vals = $this->extractRowInfo($row); + if($vals) $data[] = $vals; } $db->freeResult($res); @@ -109,6 +126,59 @@ class ApiQueryRecentChanges extends ApiQueryBase { $result->addValue('query', $this->getModuleName(), $data); } + private function extractRowInfo($row) { + $movedToTitle = false; + if (!empty($row->rc_moved_to_title)) + $movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title); + + $title = Title :: makeTitle($row->rc_namespace, $row->rc_title); + $vals = array (); + + $vals['type'] = intval($row->rc_type); + + if ($this->fld_title) { + ApiQueryBase :: addTitleInfo($vals, $title); + if ($movedToTitle) + ApiQueryBase :: addTitleInfo($vals, $movedToTitle, "new_"); + } + + if ($this->fld_ids) { + $vals['rcid'] = intval($row->rc_id); + $vals['pageid'] = intval($row->rc_cur_id); + $vals['revid'] = intval($row->rc_this_oldid); + $vals['old_revid'] = intval( $row->rc_last_oldid ); + } + + if ($this->fld_user) { + $vals['user'] = $row->rc_user_text; + if(!$row->rc_user) + $vals['anon'] = ''; + } + + if ($this->fld_flags) { + if ($row->rc_bot) + $vals['bot'] = ''; + if ($row->rc_new) + $vals['new'] = ''; + if ($row->rc_minor) + $vals['minor'] = ''; + } + + if ($this->fld_sizes) { + $vals['oldlen'] = intval($row->rc_old_len); + $vals['newlen'] = intval($row->rc_new_len); + } + + if ($this->fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp); + + if ($this->fld_comment && !empty ($row->rc_comment)) { + $vals['comment'] = $row->rc_comment; + } + + return $vals; + } + protected function getAllowedParams() { return array ( 'start' => array ( @@ -130,10 +200,15 @@ class ApiQueryRecentChanges extends ApiQueryBase { ), 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'title|timestamp|ids', ApiBase :: PARAM_TYPE => array ( 'user', 'comment', - 'flags' + 'flags', + 'timestamp', + 'title', + 'ids', + 'sizes' ) ), 'show' => array ( @@ -151,7 +226,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ) ); @@ -183,7 +258,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 24100 2007-07-15 01:12:54Z yurik $'; } } -?> + diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index fc5f6241..2672478b 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,10 @@ if (!defined('MEDIAWIKI')) { } /** + * A query action to enumerate revisions of a given page, or show top revisions of multiple pages. + * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev. + * In the enumeration mode, ranges of revisions may be requested and filtered. + * * @addtogroup API */ class ApiQueryRevisions extends ApiQueryBase { @@ -37,15 +41,18 @@ class ApiQueryRevisions extends ApiQueryBase { parent :: __construct($query, $moduleName, 'rv'); } + private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, + $fld_comment = false, $fld_user = false, $fld_content = false; + public function execute() { - $limit = $startid = $endid = $start = $end = $dir = $prop = null; + $limit = $startid = $endid = $start = $end = $dir = $prop = $user = $excludeuser = null; extract($this->extractRequestParams()); // If any of those parameters are used, work in 'enumeration' mode. // Enum mode can only be used when exactly one page is provided. // Enumerating revisions on multiple pages make it extremelly // difficult to manage continuations and require additional sql indexes - $enumRevMode = (!is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end)); + $enumRevMode = (!is_null($user) || !is_null($excludeuser) || !is_null($limit) || !is_null($startid) || !is_null($endid) || $dir === 'newer' || !is_null($start) || !is_null($end)); $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); @@ -59,39 +66,50 @@ class ApiQueryRevisions extends ApiQueryBase { $this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids'); if ($pageCount > 1 && $enumRevMode) - $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, start, and end parameters may only be used on a single page.', 'multpages'); + $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start, and end parameters may only be used on a single page.', 'multpages'); $this->addTables('revision'); - $this->addFields(array ( - 'rev_id', - 'rev_page', - 'rev_text_id', - 'rev_minor_edit' - )); $this->addWhere('rev_deleted=0'); - $showContent = false; + $prop = array_flip($prop); - if (!is_null($prop)) { - $prop = array_flip($prop); - $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp'])); - $this->addFieldsIf('rev_comment', isset ($prop['comment'])); - if (isset ($prop['user'])) { - $this->addFields('rev_user'); - $this->addFields('rev_user_text'); - } - if (isset ($prop['content'])) { - $this->addTables('text'); - $this->addWhere('rev_text_id=old_id'); - $this->addFields('old_id'); - $this->addFields('old_text'); - $this->addFields('old_flags'); - $showContent = true; + // These field are needed regardless of the client requesting them + $this->addFields('rev_id'); + $this->addFields('rev_page'); + + // Optional fields + $this->fld_ids = isset ($prop['ids']); + // $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed? + $this->fld_flags = $this->addFieldsIf('rev_minor_edit', isset ($prop['flags'])); + $this->fld_timestamp = $this->addFieldsIf('rev_timestamp', isset ($prop['timestamp'])); + $this->fld_comment = $this->addFieldsIf('rev_comment', isset ($prop['comment'])); + $this->fld_size = $this->addFieldsIf('rev_len', isset ($prop['size'])); + + if (isset ($prop['user'])) { + $this->addFields('rev_user'); + $this->addFields('rev_user_text'); + $this->fld_user = true; + } + if (isset ($prop['content'])) { + + // For each page we will request, the user must have read rights for that page + foreach ($pageSet->getGoodTitles() as $title) { + if( !$title->userCanRead() ) + $this->dieUsage( + 'The current user is not allowed to read ' . $title->getPrefixedText(), + 'accessdenied'); } + + $this->addTables('text'); + $this->addWhere('rev_text_id=old_id'); + $this->addFields('old_id'); + $this->addFields('old_text'); + $this->addFields('old_flags'); + $this->fld_content = true; } - $userMax = ($showContent ? 50 : 500); - $botMax = ($showContent ? 200 : 10000); + $userMax = ($this->fld_content ? 50 : 500); + $botMax = ($this->fld_content ? 200 : 10000); if ($enumRevMode) { @@ -102,6 +120,9 @@ class ApiQueryRevisions extends ApiQueryBase { if (!is_null($endid) && !is_null($end)) $this->dieUsage('end and endid cannot be used together', 'badparams'); + if(!is_null($user) && !is_null( $excludeuser)) + $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); + // This code makes an assumption that sorting by rev_id and rev_timestamp produces // the same result. This way users may request revisions starting at a given time, // but to page through results use the rev_id returned after each page. @@ -117,10 +138,25 @@ class ApiQueryRevisions extends ApiQueryBase { // must manually initialize unset limit if (is_null($limit)) $limit = 10; - $this->validateLimit($this->encodeParamName('limit'), $limit, 1, $userMax, $botMax); + $this->validateLimit('limit', $limit, 1, $userMax, $botMax); // There is only one ID, use it $this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles()))); + + if(!is_null($user)) { + $this->addWhereFld('rev_user_text', $user); + } elseif (!is_null( $excludeuser)) { + $this->addWhere('rev_user_text != ' . $this->getDB()->addQuotes($excludeuser)); + } + } + elseif ($revCount > 0) { + $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax); + + // Get all revision IDs + $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs())); + + // assumption testing -- we should never get more then $revCount rows. + $limit = $revCount; } elseif ($pageCount > 0) { // When working in multi-page non-enumeration mode, @@ -133,15 +169,8 @@ class ApiQueryRevisions extends ApiQueryBase { // Get all page IDs $this->addWhereFld('page_id', array_keys($pageSet->getGoodTitles())); - $limit = $pageCount; // assumption testing -- we should never get more then $pageCount rows. - } - elseif ($revCount > 0) { - $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax); - - // Get all revision IDs - $this->addWhereFld('rev_id', array_keys($pageSet->getRevisionIDs())); - - $limit = $revCount; // assumption testing -- we should never get more then $revCount rows. + // assumption testing -- we should never get more then $pageCount rows. + $limit = $pageCount; } else ApiBase :: dieDebug(__METHOD__, 'param validation?'); @@ -158,21 +187,18 @@ class ApiQueryRevisions extends ApiQueryBase { // We've reached the one extra which shows that there are additional pages to be had. Stop here... if (!$enumRevMode) ApiBase :: dieDebug(__METHOD__, 'Got more rows then expected'); // bug report - $this->setContinueEnumParameter('startid', $row->rev_id); + $this->setContinueEnumParameter('startid', intval($row->rev_id)); break; } - $vals = $this->addRowInfo('rev', $row); - if ($vals) { - if ($showContent) - ApiResult :: setContent($vals, Revision :: getRevisionText($row)); - - $this->getResult()->addValue(array ( + $this->getResult()->addValue( + array ( 'query', 'pages', - intval($row->rev_page - ), 'revisions'), intval($row->rev_id), $vals); - } + intval($row->rev_page), + 'revisions'), + null, + $this->extractRowInfo($row)); } $db->freeResult($res); @@ -188,21 +214,62 @@ class ApiQueryRevisions extends ApiQueryBase { } } + private function extractRowInfo($row) { + + $vals = array (); + + if ($this->fld_ids) { + $vals['revid'] = intval($row->rev_id); + // $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed? + } + + if ($this->fld_flags && $row->rev_minor_edit) + $vals['minor'] = ''; + + if ($this->fld_user) { + $vals['user'] = $row->rev_user_text; + if (!$row->rev_user) + $vals['anon'] = ''; + } + + if ($this->fld_timestamp) { + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); + } + + if ($this->fld_size && !is_null($row->rev_len)) { + $vals['size'] = intval($row->rev_len); + } + + if ($this->fld_comment && !empty ($row->rev_comment)) { + $vals['comment'] = $row->rev_comment; + } + + if ($this->fld_content) { + ApiResult :: setContent($vals, Revision :: getRevisionText($row)); + } + + return $vals; + } + protected function getAllowedParams() { return array ( 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'ids|timestamp|flags|comment|user', ApiBase :: PARAM_TYPE => array ( + 'ids', + 'flags', 'timestamp', 'user', + 'size', 'comment', - 'content' + 'content', ) ), 'limit' => array ( ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_SML1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_SML1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_SML2 ), 'startid' => array ( @@ -223,6 +290,12 @@ class ApiQueryRevisions extends ApiQueryBase { 'newer', 'older' ) + ), + 'user' => array( + ApiBase :: PARAM_TYPE => 'user' + ), + 'excludeuser' => array( + ApiBase :: PARAM_TYPE => 'user' ) ); } @@ -235,7 +308,9 @@ class ApiQueryRevisions extends ApiQueryBase { 'endid' => 'stop revision enumeration on this revid (enum)', 'start' => 'from which revision timestamp to start enumeration (enum)', 'end' => 'enumerate up to this timestamp (enum)', - 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)' + 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)', + 'user' => 'only include revisions made by user', + 'excludeuser' => 'exclude revisions made by user', ); } @@ -259,12 +334,16 @@ class ApiQueryRevisions extends ApiQueryBase { 'Get first 5 revisions of the "Main Page":', ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer', 'Get first 5 revisions of the "Main Page" made after 2006-05-01:', - ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000' + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000', + 'Get first 5 revisions of the "Main Page" that were not made made by anonymous user "127.0.0.1"', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvexcludeuser=127.0.0.1', + 'Get first 5 revisions of the "Main Page" that were made by the user "MediaWiki default"', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvuser=MediaWiki%20default', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRevisions.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryRevisions.php 25407 2007-09-02 14:00:11Z tstarling $'; } } -?> + diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php new file mode 100644 index 00000000..268616b1 --- /dev/null +++ b/includes/api/ApiQuerySearch.php @@ -0,0 +1,151 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to perform full text search within wiki titles and content + * + * @addtogroup API + */ +class ApiQuerySearch extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'sr'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + + $params = $this->extractRequestParams(); + + $limit = $params['limit']; + $query = $params['search']; + if (is_null($query) || empty($query)) + $this->dieUsage("empty search string is not allowed", 'param-search'); + + $search = SearchEngine::create(); + $search->setLimitOffset( $limit+1, $params['offset'] ); + $search->setNamespaces( $params['namespace'] ); + $search->showRedirects = $params['redirects']; + + if ($params['what'] == 'text') + $matches = $search->searchText( $query ); + else + $matches = $search->searchTitle( $query ); + + $data = array (); + $count = 0; + while( $result = $matches->next() ) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional items to be had. Stop here... + $this->setContinueEnumParameter('offset', $params['offset'] + $params['limit']); + break; + } + + $title = $result->getTitle(); + if (is_null($resultPageSet)) { + $data[] = array( + 'ns' => intval($title->getNamespace()), + 'title' => $title->getPrefixedText()); + } else { + $data[] = $title; + } + } + + if (is_null($resultPageSet)) { + $result = $this->getResult(); + $result->setIndexedTagName($data, 'p'); + $result->addValue('query', $this->getModuleName(), $data); + } else { + $resultPageSet->populateFromTitles($data); + } + } + + protected function getAllowedParams() { + return array ( + 'search' => null, + 'namespace' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => 'namespace', + ApiBase :: PARAM_ISMULTI => true, + ), + 'what' => array ( + ApiBase :: PARAM_DFLT => 'title', + ApiBase :: PARAM_TYPE => array ( + 'title', + 'text', + ) + ), + 'redirects' => false, + 'offset' => 0, + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'search' => 'Search for all page titles (or content) that has this value.', + 'namespace' => 'The namespace(s) to enumerate.', + 'what' => 'Search inside the text or titles.', + 'redirects' => 'Include redirect pages in the search.', + 'offset' => 'Use this value to continue paging (return by query)', + 'limit' => 'How many total pages to return.' + ); + } + + protected function getDescription() { + return 'Perform a full text search'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=search&srsearch=meaning', + 'api.php?action=query&list=search&srwhat=text&srsearch=meaning', + 'api.php?action=query&generator=search&gsrsearch=meaning&prop=info', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQuerySearch.php 24453 2007-07-30 08:09:15Z yurik $'; + } +} + diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index fa185c97..1fa3d8fc 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * A query action to return meta information about the wiki site. + * * @addtogroup API */ class ApiQuerySiteinfo extends ApiQueryBase { @@ -38,58 +40,164 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function execute() { - $prop = null; - extract($this->extractRequestParams()); - foreach ($prop as $p) { - switch ($p) { + $params = $this->extractRequestParams(); + foreach ($params['prop'] as $p) { + switch ($p) { + default : + ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); case 'general' : - - global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText; - $data = array (); - $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); - $data['mainpage'] = $mainPage->getText(); - $data['base'] = $mainPage->getFullUrl(); - $data['sitename'] = $wgSitename; - $data['generator'] = "MediaWiki $wgVersion"; - $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future - if (isset($wgRightsCode)) - $data['rightscode'] = $wgRightsCode; - $data['rights'] = $wgRightsText; - $this->getResult()->addValue('query', $p, $data); + $this->appendGeneralInfo($p); break; - case 'namespaces' : - - global $wgContLang; - $data = array (); - foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) { - $data[$ns] = array ( - 'id' => $ns - ); - ApiResult :: setContent($data[$ns], $title); - } - $this->getResult()->setIndexedTagName($data, 'ns'); - $this->getResult()->addValue('query', $p, $data); + $this->appendNamespaces($p); + break; + case 'interwikimap' : + $filteriw = isset($params['filteriw']) ? $params['filteriw'] : false; + $this->appendInterwikiMap($p, $filteriw); + break; + case 'dbrepllag' : + $this->appendDbReplLagInfo($p, $params['showalldb']); + break; + case 'statistics' : + $this->appendStatistics($p); break; - - default : - ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); } } } + protected function appendGeneralInfo($property) { + global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgLanguageCode; + + $data = array (); + $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); + $data['mainpage'] = $mainPage->getText(); + $data['base'] = $mainPage->getFullUrl(); + $data['sitename'] = $wgSitename; + $data['generator'] = "MediaWiki $wgVersion"; + $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future + if (isset($wgRightsCode)) + $data['rightscode'] = $wgRightsCode; + $data['rights'] = $wgRightsText; + $data['lang'] = $wgLanguageCode; + + $this->getResult()->addValue('query', $property, $data); + } + + protected function appendNamespaces($property) { + global $wgContLang; + + $data = array (); + foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) { + $data[$ns] = array ( + 'id' => $ns + ); + ApiResult :: setContent($data[$ns], $title); + } + + $this->getResult()->setIndexedTagName($data, 'ns'); + $this->getResult()->addValue('query', $property, $data); + } + + protected function appendInterwikiMap($property, $filter) { + + $this->resetQueryParams(); + $this->addTables('interwiki'); + $this->addFields(array('iw_prefix', 'iw_local', 'iw_url')); + + if($filter === 'local') { + $this->addWhere('iw_local = 1'); + } elseif($filter === '!local') { + $this->addWhere('iw_local = 0'); + } elseif($filter !== false) { + ApiBase :: dieDebug(__METHOD__, "Unknown filter=$filter"); + } + + $this->addOption('ORDER BY', 'iw_prefix'); + + $db = $this->getDB(); + $res = $this->select(__METHOD__); + + $data = array(); + while($row = $db->fetchObject($res)) + { + $val['prefix'] = $row->iw_prefix; + if ($row->iw_local == '1') + $val['local'] = ''; +// $val['trans'] = intval($row->iw_trans); // should this be exposed? + $val['url'] = $row->iw_url; + + $data[] = $val; + } + $db->freeResult($res); + + $this->getResult()->setIndexedTagName($data, 'iw'); + $this->getResult()->addValue('query', $property, $data); + } + + protected function appendDbReplLagInfo($property, $includeAll) { + global $wgLoadBalancer, $wgShowHostnames; + + $data = array(); + + if ($includeAll) { + if (!$wgShowHostnames) + $this->dieUsage('Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied'); + + global $wgDBservers; + $lags = $wgLoadBalancer->getLagTimes(); + foreach( $lags as $i => $lag ) { + $data[] = array ( + 'host' => $wgDBservers[$i]['host'], + 'lag' => $lag); + } + } else { + list( $host, $lag ) = $wgLoadBalancer->getMaxLag(); + $data[] = array ( + 'host' => $wgShowHostnames ? $host : '', + 'lag' => $lag); + } + + $result = $this->getResult(); + $result->setIndexedTagName($data, 'db'); + $result->addValue('query', $property, $data); + } + + protected function appendStatistics($property) { + $data = array (); + $data['pages'] = intval(SiteStats::pages()); + $data['articles'] = intval(SiteStats::articles()); + $data['views'] = intval(SiteStats::views()); + $data['edits'] = intval(SiteStats::edits()); + $data['images'] = intval(SiteStats::images()); + $data['users'] = intval(SiteStats::users()); + $data['admins'] = intval(SiteStats::admins()); + $data['jobs'] = intval(SiteStats::jobs()); + $this->getResult()->addValue('query', $property, $data); + } + protected function getAllowedParams() { return array ( + 'prop' => array ( ApiBase :: PARAM_DFLT => 'general', ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array ( 'general', - 'namespaces' - ) - ) + 'namespaces', + 'interwikimap', + 'dbrepllag', + 'statistics', + )), + + 'filteriw' => array ( + ApiBase :: PARAM_TYPE => array ( + 'local', + '!local', + )), + + 'showalldb' => false, ); } @@ -97,9 +205,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { return array ( 'prop' => array ( 'Which sysinfo properties to get:', - ' "general" - Overall system information', - ' "namespaces" - List of registered namespaces (localized)' - ) + ' "general" - Overall system information', + ' "namespaces" - List of registered namespaces (localized)', + ' "statistics" - Returns site statistics', + ' "interwikimap" - Returns interwiki map (optionally filtered)', + ' "dbrepllag" - Returns database server with the highest replication lag', + ), + 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', + 'showalldb' => 'List all database servers, not just the one lagging the most', ); } @@ -108,11 +221,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function getExamples() { - return 'api.php?action=query&meta=siteinfo&siprop=general|namespaces'; + return array( + 'api.php?action=query&meta=siteinfo&siprop=general|namespaces|statistics', + 'api.php?action=query&meta=siteinfo&siprop=interwikimap&sifilteriw=local', + 'api.php?action=query&meta=siteinfo&siprop=dbrepllag&sishowalldb', + ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 25238 2007-08-28 15:37:31Z robchurch $'; } -} -?> +} \ No newline at end of file diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 05bfbb20..05c3d945 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,8 @@ if (!defined('MEDIAWIKI')) { } /** + * This query action adds a list of a specified user's contributions to the output. + * * @addtogroup API */ class ApiQueryContributions extends ApiQueryBase { @@ -37,83 +39,49 @@ class ApiQueryContributions extends ApiQueryBase { parent :: __construct($query, $moduleName, 'uc'); } + private $params, $username; + private $fld_ids = false, $fld_title = false, $fld_timestamp = false, + $fld_comment = false, $fld_flags = false; + public function execute() { - //Blank all our variables - $limit = $user = $start = $end = $dir = null; + // Parse some parameters + $this->params = $this->extractRequestParams(); - //Get our parameters out - extract($this->extractRequestParams()); + $prop = array_flip($this->params['prop']); + $this->fld_ids = isset($prop['ids']); + $this->fld_title = isset($prop['title']); + $this->fld_comment = isset($prop['comment']); + $this->fld_flags = isset($prop['flags']); + $this->fld_timestamp = isset($prop['timestamp']); - //Get a database instance + // TODO: if the query is going only against the revision table, should this be done? + $this->selectNamedDB('contributions', DB_SLAVE, 'contributions'); $db = $this->getDB(); - if (is_null($user)) - $this->dieUsage("User parameter may not be empty", 'param_user'); - $userid = $db->selectField('user', 'user_id', array ( - 'user_name' => $user - )); - if (!$userid) - $this->dieUsage("User name $user not found", 'param_user'); - - //Get the table names - list ($tbl_page, $tbl_revision) = $db->tableNamesN('page', 'revision'); - - //We're after the revision table, and the corresponding page row for - //anything we retrieve. - $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON " . - "page_id=rev_page"); - - //We want to know the namespace, title, new-ness, and ID of a page, - // and the id, text-id, timestamp, minor-status, summary and page - // of a revision. - $this->addFields(array('page_namespace', 'page_title', 'page_is_new', - 'rev_id', 'rev_text_id', 'rev_timestamp', 'rev_minor_edit', - 'rev_comment', 'rev_page')); - - // We only want pages by the specified user. - $this->addWhereFld('rev_user_text', $user); - // ... and in the specified timeframe. - $this->addWhereRange('rev_timestamp', $dir, $start, $end ); + // Prepare query + $this->prepareUsername(); + $this->prepareQuery(); - $this->addOption('LIMIT', $limit + 1); + //Do the actual query. + $res = $this->select( __METHOD__ ); //Initialise some variables $data = array (); $count = 0; - - //Do the actual query. - $res = $this->select( __METHOD__ ); + $limit = $this->params['limit']; //Fetch each row while ( $row = $db->fetchObject( $res ) ) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('start', $row->rev_timestamp); + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp)); break; } - //There's a fancy function in ApiQueryBase that does - // most of the work for us. Use that for the page - // and revision. - $revvals = $this->addRowInfo('rev', $row); - $pagevals = $this->addRowInfo('page', $row); - - //If we got data on the revision only, use only - // that data. - if($revvals && !$pagevals) { - $data[] = $revvals; - } - //If we got data on the page only, use only - // that data. - else if($pagevals && !$revvals) { - $data[] = $pagevals; - } - //... and if we got data on both the revision and - // the page, merge the data and send it out. - else if($pagevals && $revvals) { - $data[] = array_merge($revvals, $pagevals); - } + $vals = $this->extractRowInfo($row); + if ($vals) + $data[] = $vals; } //Free the database record so the connection can get on with other stuff @@ -124,13 +92,117 @@ class ApiQueryContributions extends ApiQueryBase { $this->getResult()->addValue('query', $this->getModuleName(), $data); } + /** + * Validate the 'user' parameter and set the value to compare + * against `revision`.`rev_user_text` + */ + private function prepareUsername() { + $user = $this->params['user']; + if( $user ) { + $name = User::isIP( $user ) + ? $user + : User::getCanonicalName( $user, 'valid' ); + if( $name === false ) { + $this->dieUsage( "User name {$user} is not valid", 'param_user' ); + } else { + $this->username = $name; + } + } else { + $this->dieUsage( 'User parameter may not be empty', 'param_user' ); + } + } + + /** + * Prepares the query and returns the limit of rows requested + */ + private function prepareQuery() { + + //We're after the revision table, and the corresponding page row for + //anything we retrieve. + list ($tbl_page, $tbl_revision) = $this->getDB()->tableNamesN('page', 'revision'); + $this->addTables("$tbl_revision LEFT OUTER JOIN $tbl_page ON page_id=rev_page"); + + $this->addWhereFld('rev_deleted', 0); + + // We only want pages by the specified user. + $this->addWhereFld( 'rev_user_text', $this->username ); + + // ... and in the specified timeframe. + $this->addWhereRange('rev_timestamp', + $this->params['dir'], $this->params['start'], $this->params['end'] ); + + $this->addWhereFld('page_namespace', $this->params['namespace']); + + $show = $this->params['show']; + if (!is_null($show)) { + $show = array_flip($show); + if (isset ($show['minor']) && isset ($show['!minor'])) + $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); + + $this->addWhereIf('rev_minor_edit = 0', isset ($show['!minor'])); + $this->addWhereIf('rev_minor_edit != 0', isset ($show['minor'])); + } + + $this->addOption('LIMIT', $this->params['limit'] + 1); + + // Mandatory fields: timestamp allows request continuation + // ns+title checks if the user has access rights for this page + $this->addFields(array( + 'rev_timestamp', + 'page_namespace', + 'page_title', + )); + + $this->addFieldsIf('rev_page', $this->fld_ids); + $this->addFieldsIf('rev_id', $this->fld_ids); + // $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed? + $this->addFieldsIf('rev_comment', $this->fld_comment); + $this->addFieldsIf('rev_minor_edit', $this->fld_flags); + + // These fields depend only work if the page table is joined + $this->addFieldsIf('page_is_new', $this->fld_flags); + } + + /** + * Extract fields from the database row and append them to a result array + */ + private function extractRowInfo($row) { + + $vals = array(); + + if ($this->fld_ids) { + $vals['pageid'] = intval($row->rev_page); + $vals['revid'] = intval($row->rev_id); + // $vals['textid'] = intval($row->rev_text_id); // todo: Should this field be exposed? + } + + if ($this->fld_title) + ApiQueryBase :: addTitleInfo($vals, + Title :: makeTitle($row->page_namespace, $row->page_title)); + + if ($this->fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); + + if ($this->fld_flags) { + if ($row->page_is_new) + $vals['new'] = ''; + if ($row->rev_minor_edit) + $vals['minor'] = ''; + } + + if ($this->fld_comment && !empty ($row->rev_comment)) + $vals['comment'] = $row->rev_comment; + + return $vals; + } + protected function getAllowedParams() { return array ( 'limit' => array ( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ), 'start' => array ( @@ -139,14 +211,38 @@ class ApiQueryContributions extends ApiQueryBase { 'end' => array ( ApiBase :: PARAM_TYPE => 'timestamp' ), - 'user' => null, + 'user' => array ( + ApiBase :: PARAM_TYPE => 'user' + ), 'dir' => array ( ApiBase :: PARAM_DFLT => 'older', ApiBase :: PARAM_TYPE => array ( 'newer', 'older' ) - ) + ), + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace' + ), + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'ids|title|timestamp|flags|comment', + ApiBase :: PARAM_TYPE => array ( + 'ids', + 'title', + 'timestamp', + 'comment', + 'flags' + ) + ), + 'show' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'minor', + '!minor', + ) + ), ); } @@ -156,12 +252,15 @@ class ApiQueryContributions extends ApiQueryBase { 'start' => 'The start timestamp to return from.', 'end' => 'The end timestamp to return to.', 'user' => 'The user to retrieve contributions for.', - 'dir' => 'The direction to search (older or newer).' + 'dir' => 'The direction to search (older or newer).', + 'namespace' => 'Only list contributions in these namespaces', + 'prop' => 'Include additional pieces of information', + 'show' => 'Show only items that meet this criteria, e.g. non minor edits only: show=!minor', ); } protected function getDescription() { - return 'Get edits by a user..'; + return 'Get all edits by a user'; } protected function getExamples() { @@ -171,7 +270,7 @@ class ApiQueryContributions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserContributions.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryUserContributions.php 24754 2007-08-13 18:18:18Z robchurch $'; } } -?> + diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php new file mode 100644 index 00000000..a41b8679 --- /dev/null +++ b/includes/api/ApiQueryUserInfo.php @@ -0,0 +1,133 @@ +@gmail.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ('ApiQueryBase.php'); +} + +/** + * Query module to get information about the currently logged-in user + * + * @addtogroup API + */ +class ApiQueryUserInfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'ui'); + } + + public function execute() { + + global $wgUser; + + $params = $this->extractRequestParams(); + $result = $this->getResult(); + + $vals = array(); + $vals['name'] = $wgUser->getName(); + + if( $wgUser->isAnon() ) $vals['anon'] = ''; + + if (!is_null($params['prop'])) { + $prop = array_flip($params['prop']); + if (isset($prop['blockinfo'])) { + if ($wgUser->isBlocked()) { + $vals['blockedby'] = User::whoIs($wgUser->blockedBy()); + $vals['blockreason'] = $wgUser->blockedFor(); + } + } + if (isset($prop['hasmsg']) && $wgUser->getNewtalk()) { + $vals['messages'] = ''; + } + if (isset($prop['groups'])) { + $vals['groups'] = $wgUser->getGroups(); + $result->setIndexedTagName($vals['groups'], 'g'); // even if empty + } + if (isset($prop['rights'])) { + $vals['rights'] = $wgUser->getRights(); + $result->setIndexedTagName($vals['rights'], 'r'); // even if empty + } + } + + if (!empty($params['option'])) { + foreach( $params['option'] as $option ) { + if (empty($option)) + $this->dieUsage('Empty value is not allowed for the option parameter', 'option'); + $vals['options'][$option] = $wgUser->getOption($option); + } + } + + $result->addValue(null, $this->getModuleName(), $vals); + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_DFLT => NULL, + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'blockinfo', + 'hasmsg', + 'groups', + 'rights', + )), + 'option' => array ( + ApiBase :: PARAM_DFLT => NULL, + ApiBase :: PARAM_ISMULTI => true, + ), + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => array( + 'What pieces of information to include', + ' blockinfo - tags if the user is blocked, by whom, and for what reason', + ' hasmsg - adds a tag "message" if user has pending messages', + ' groups - lists all the groups the current user belongs to', + ' rights - lists of all rights the current user has', + ), + 'option' => 'A list of user preference options to get', + ); + } + + protected function getDescription() { + return 'Get information about the current user'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&meta=userinfo', + 'api.php?action=query&meta=userinfo&uiprop=blockinfo|groups|rights|hasmsg', + 'api.php?action=query&meta=userinfo&uioption=rememberpassword', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryUserInfo.php 24529 2007-08-01 20:11:29Z yurik $'; + } +} + diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 73c31abb..16586a40 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,9 @@ if (!defined('MEDIAWIKI')) { } /** + * This query action allows clients to retrieve a list of recently modified pages + * that are part of the logged-in user's watchlist. + * * @addtogroup API */ class ApiQueryWatchlist extends ApiQueryGeneratorBase { @@ -45,8 +48,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->run($resultPageSet); } + private $fld_ids = false, $fld_title = false, $fld_patrol = false, $fld_flags = false, + $fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_sizes = false; + private function run($resultPageSet = null) { - global $wgUser; + global $wgUser, $wgDBtype; + + $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist'); if (!$wgUser->isLoggedIn()) $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); @@ -54,17 +62,20 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $allrev = $start = $end = $namespace = $dir = $limit = $prop = null; extract($this->extractRequestParams()); - $patrol = $timestamp = $user = $comment = false; - if (!is_null($prop)) { - if (!is_null($resultPageSet)) - $this->dieUsage('prop parameter may not be used in a generator', 'params'); + if (!is_null($prop) && is_null($resultPageSet)) { + + $prop = array_flip($prop); - $user = (false !== array_search('user', $prop)); - $comment = (false !== array_search('comment', $prop)); - $timestamp = (false !== array_search('timestamp', $prop)); // TODO: $timestamp not currently being used. - $patrol = (false !== array_search('patrol', $prop)); + $this->fld_ids = isset($prop['ids']); + $this->fld_title = isset($prop['title']); + $this->fld_flags = isset($prop['flags']); + $this->fld_user = isset($prop['user']); + $this->fld_comment = isset($prop['comment']); + $this->fld_timestamp = isset($prop['timestamp']); + $this->fld_sizes = isset($prop['sizes']); + $this->fld_patrol = isset($prop['patrol']); - if ($patrol) { + if ($this->fld_patrol) { global $wgUseRCPatrol, $wgUser; if (!$wgUseRCPatrol || !$wgUser->isAllowed('patrol')) $this->dieUsage('patrol property is not available', 'patrol'); @@ -77,15 +88,17 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'rc_this_oldid', 'rc_namespace', 'rc_title', - 'rc_new', - 'rc_minor', 'rc_timestamp' )); - $this->addFieldsIf('rc_user', $user); - $this->addFieldsIf('rc_user_text', $user); - $this->addFieldsIf('rc_comment', $comment); - $this->addFieldsIf('rc_patrolled', $patrol); + $this->addFieldsIf('rc_new', $this->fld_flags); + $this->addFieldsIf('rc_minor', $this->fld_flags); + $this->addFieldsIf('rc_user', $this->fld_user); + $this->addFieldsIf('rc_user_text', $this->fld_user); + $this->addFieldsIf('rc_comment', $this->fld_comment); + $this->addFieldsIf('rc_patrolled', $this->fld_patrol); + $this->addFieldsIf('rc_old_len', $this->fld_sizes); + $this->addFieldsIf('rc_new_len', $this->fld_sizes); } elseif ($allrev) { $this->addFields(array ( @@ -114,12 +127,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'wl_namespace = rc_namespace', 'wl_title = rc_title', 'rc_cur_id = page_id', - 'wl_user' => $userId + 'wl_user' => $userId, + 'rc_deleted' => 0, )); + $this->addWhereRange('rc_timestamp', $dir, $start, $end); $this->addWhereFld('wl_namespace', $namespace); $this->addWhereIf('rc_this_oldid=page_latest', !$allrev); - $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end)); + + # This is a index optimization for mysql, as done in the Special:Watchlist page + $this->addWhereIf("rc_timestamp > ''", !isset ($start) && !isset ($end) && $wgDBtype == 'mysql'); $this->addOption('LIMIT', $limit +1); @@ -131,23 +148,19 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter('start', $row->rc_timestamp); + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp)); break; } if (is_null($resultPageSet)) { - $vals = $this->addRowInfo('rc', $row); - if($vals) + $vals = $this->extractRowInfo($row); + if ($vals) $data[] = $vals; } else { - $title = Title :: makeTitle($row->rc_namespace, $row->rc_title); - // skip any pages that user has no rights to read - if ($title->userCanRead()) { - if ($allrev) { - $data[] = intval($row->rc_this_oldid); - } else { - $data[] = intval($row->rc_cur_id); - } + if ($allrev) { + $data[] = intval($row->rc_this_oldid); + } else { + $data[] = intval($row->rc_cur_id); } } } @@ -165,6 +178,50 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } } + private function extractRowInfo($row) { + + $vals = array (); + + if ($this->fld_ids) { + $vals['pageid'] = intval($row->rc_cur_id); + $vals['revid'] = intval($row->rc_this_oldid); + } + + if ($this->fld_title) + ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->rc_namespace, $row->rc_title)); + + if ($this->fld_user) { + $vals['user'] = $row->rc_user_text; + if (!$row->rc_user) + $vals['anon'] = ''; + } + + if ($this->fld_flags) { + if ($row->rc_new) + $vals['new'] = ''; + if ($row->rc_minor) + $vals['minor'] = ''; + } + + if ($this->fld_patrol && isset($row->rc_patrolled)) + $vals['patrolled'] = ''; + + if ($this->fld_timestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp); + + $this->addFieldsIf('rc_new_len', $this->fld_sizes); + + if ($this->fld_sizes) { + $vals['oldlen'] = intval($row->rc_old_len); + $vals['newlen'] = intval($row->rc_new_len); + } + + if ($this->fld_comment && !empty ($row->rc_comment)) + $vals['comment'] = $row->rc_comment; + + return $vals; + } + protected function getAllowedParams() { return array ( 'allrev' => false, @@ -189,16 +246,21 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, - ApiBase :: PARAM_MAX1 => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ), 'prop' => array ( APIBase :: PARAM_ISMULTI => true, + APIBase :: PARAM_DFLT => 'ids|title|flags', APIBase :: PARAM_TYPE => array ( + 'ids', + 'title', + 'flags', 'user', 'comment', 'timestamp', - 'patrol' + 'patrol', + 'sizes', ) ) ); @@ -223,14 +285,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { protected function getExamples() { return array ( 'api.php?action=query&list=watchlist', - 'api.php?action=query&list=watchlist&wlallrev', + 'api.php?action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment', + 'api.php?action=query&list=watchlist&wlallrev&wlprop=ids|title|timestamp|user|comment', 'api.php?action=query&generator=watchlist&prop=info', 'api.php?action=query&generator=watchlist&gwlallrev&prop=revisions&rvprop=timestamp|user' ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryWatchlist.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiQueryWatchlist.php 24092 2007-07-14 19:04:31Z yurik $'; } } -?> + diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 79fd34a1..a318d808 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -5,7 +5,7 @@ * * API for MediaWiki 1.8+ * - * Copyright (C) 2006 Yuri Astrakhan + * Copyright (C) 2006 Yuri Astrakhan @gmail.com * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,20 @@ if (!defined('MEDIAWIKI')) { } /** + * This class represents the result of the API operations. + * It simply wraps a nested array() structure, adding some functions to simplify array's modifications. + * As various modules execute, they add different pieces of information to this result, + * structuring it as it will be given to the client. + * + * Each subarray may either be a dictionary - key-value pairs with unique keys, + * or lists, where the items are added using $data[] = $value notation. + * + * There are two special key values that change how XML output is generated: + * '_element' This key sets the tag name for the rest of the elements in the current array. + * It is only inserted if the formatter returned true for getNeedsRawData() + * '*' This key has special meaning only to the XML formatter, and is outputed as is + * for all others. In XML it becomes the content of the current element. + * * @addtogroup API */ class ApiResult extends ApiBase { @@ -44,6 +58,9 @@ class ApiResult extends ApiBase { $this->reset(); } + /** + * Clear the current result data. + */ public function reset() { $this->mData = array (); } @@ -56,10 +73,16 @@ class ApiResult extends ApiBase { $this->mIsRawMode = true; } + /** + * Returns true if the result is being created for the formatter that requested raw data. + */ public function getIsRawMode() { return $this->mIsRawMode; } + /** + * Get result's internal data array + */ public function & getData() { return $this->mData; } @@ -103,11 +126,6 @@ class ApiResult extends ApiBase { } } - // public static function makeContentElement($tag, $value) { - // $result = array(); - // ApiResult::setContent($result, ) - // } - // /** * In case the array contains indexed values (in addition to named), * all indexed values will have the given tag name. @@ -125,7 +143,8 @@ class ApiResult extends ApiBase { /** * Add value to the output data at the given path. * Path is an indexed array, each element specifing the branch at which to add the new value - * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value + * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value + * If $name is empty, the $value is added as a next list element data[] = $value */ public function addValue($path, $name, $value) { @@ -145,7 +164,10 @@ class ApiResult extends ApiBase { } } - ApiResult :: setElement($data, $name, $value); + if (empty($name)) + $data[] = $value; // Add list element + else + ApiResult :: setElement($data, $name, $value); // Add named element } public function execute() { @@ -153,7 +175,7 @@ class ApiResult extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiResult.php 21402 2007-04-20 08:55:14Z nickj $'; + return __CLASS__ . ': $Id: ApiResult.php 23531 2007-06-29 01:19:14Z simetrical $'; } } -?> + diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php index 59088bed..17f43500 100644 --- a/includes/cbt/CBTCompiler.php +++ b/includes/cbt/CBTCompiler.php @@ -364,4 +364,4 @@ class CBTCompiler { '; } } -?> + diff --git a/includes/cbt/CBTProcessor.php b/includes/cbt/CBTProcessor.php index 0c34204e..31d1b60a 100644 --- a/includes/cbt/CBTProcessor.php +++ b/includes/cbt/CBTProcessor.php @@ -537,4 +537,4 @@ class CBTProcessor { return new CBTValue( htmlspecialchars( $val->getText() ), $val->getDeps() ); } } -?> + diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php new file mode 100644 index 00000000..bd9ff633 --- /dev/null +++ b/includes/filerepo/ArchivedFile.php @@ -0,0 +1,108 @@ +getNamespace() == NS_IMAGE ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'filearchive', + array( + 'fa_id', + 'fa_name', + 'fa_storage_key', + 'fa_storage_group', + 'fa_size', + 'fa_bits', + 'fa_width', + 'fa_height', + 'fa_metadata', + 'fa_media_type', + 'fa_major_mime', + 'fa_minor_mime', + 'fa_description', + 'fa_user', + 'fa_user_text', + 'fa_timestamp', + 'fa_deleted' ), + array( + 'fa_name' => $title->getDbKey(), + $conds ), + __METHOD__, + array( 'ORDER BY' => 'fa_timestamp DESC' ) ); + + if ( $dbr->numRows( $res ) == 0 ) { + // this revision does not exist? + return; + } + $ret = $dbr->resultObject( $res ); + $row = $ret->fetchObject(); + + // initialize fields for filestore image object + $this->mId = intval($row->fa_id); + $this->mName = $row->fa_name; + $this->mGroup = $row->fa_storage_group; + $this->mKey = $row->fa_storage_key; + $this->mSize = $row->fa_size; + $this->mBits = $row->fa_bits; + $this->mWidth = $row->fa_width; + $this->mHeight = $row->fa_height; + $this->mMetaData = $row->fa_metadata; + $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime"; + $this->mType = $row->fa_media_type; + $this->mDescription = $row->fa_description; + $this->mUser = $row->fa_user; + $this->mUserText = $row->fa_user_text; + $this->mTimestamp = $row->fa_timestamp; + $this->mDeleted = $row->fa_deleted; + } else { + throw new MWException( 'This title does not correspond to an image page.' ); + return; + } + return true; + } + + /** + * int $field one of DELETED_* bitfield constants + * for file or revision rows + * @return bool + */ + function isDeleted( $field ) { + return ($this->mDeleted & $field) == $field; + } + + /** + * Determine if the current user is allowed to view a particular + * field of this FileStore image file, if it's marked as deleted. + * @param int $field + * @return bool + */ + function userCan( $field ) { + if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) { + // images + global $wgUser; + $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED + ? 'hiderevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; + } + } +} + + diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php new file mode 100644 index 00000000..84ec9a27 --- /dev/null +++ b/includes/filerepo/FSRepo.php @@ -0,0 +1,530 @@ +directory = $info['directory']; + $this->url = $info['url']; + + // Optional settings + $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2; + $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? + $info['deletedHashLevels'] : $this->hashLevels; + $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false; + } + + /** + * Get the public root directory of the repository. + */ + function getRootDirectory() { + return $this->directory; + } + + /** + * Get the public root URL of the repository + */ + function getRootUrl() { + return $this->url; + } + + /** + * Returns true if the repository uses a multi-level directory structure + */ + function isHashed() { + return (bool)$this->hashLevels; + } + + /** + * Get the local directory corresponding to one of the three basic zones + */ + function getZonePath( $zone ) { + switch ( $zone ) { + case 'public': + return $this->directory; + case 'temp': + return "{$this->directory}/temp"; + case 'deleted': + return $this->deletedDir; + default: + return false; + } + } + + /** + * Get the URL corresponding to one of the three basic zones + */ + function getZoneUrl( $zone ) { + switch ( $zone ) { + case 'public': + return $this->url; + case 'temp': + return "{$this->url}/temp"; + case 'deleted': + return false; // no public URL + default: + return false; + } + } + + /** + * Get a URL referring to this repository, with the private mwrepo protocol. + * The suffix, if supplied, is considered to be unencoded, and will be + * URL-encoded before being returned. + */ + function getVirtualUrl( $suffix = false ) { + $path = 'mwrepo://' . $this->name; + if ( $suffix !== false ) { + $path .= '/' . rawurlencode( $suffix ); + } + return $path; + } + + /** + * Get the local path corresponding to a virtual URL + */ + function resolveVirtualUrl( $url ) { + if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { + throw new MWException( __METHOD__.': unknown protoocl' ); + } + + $bits = explode( '/', substr( $url, 9 ), 3 ); + if ( count( $bits ) != 3 ) { + throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); + } + list( $repo, $zone, $rel ) = $bits; + if ( $repo !== $this->name ) { + throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" ); + } + $base = $this->getZonePath( $zone ); + if ( !$base ) { + throw new MWException( __METHOD__.": invalid zone: $zone" ); + } + return $base . '/' . rawurldecode( $rel ); + } + + /** + * Store a batch of files + * + * @param array $triplets (src,zone,dest) triplets as per store() + * @param integer $flags Bitwise combination of the following flags: + * self::DELETE_SOURCE Delete the source file after upload + * self::OVERWRITE Overwrite an existing destination file instead of failing + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the + * same contents as the source + */ + function storeBatch( $triplets, $flags = 0 ) { + if ( !is_writable( $this->directory ) ) { + return $this->newFatal( 'upload_directory_read_only', $this->directory ); + } + $status = $this->newGood(); + foreach ( $triplets as $i => $triplet ) { + list( $srcPath, $dstZone, $dstRel ) = $triplet; + + $root = $this->getZonePath( $dstZone ); + if ( !$root ) { + throw new MWException( "Invalid zone: $dstZone" ); + } + if ( !$this->validateFilename( $dstRel ) ) { + throw new MWException( 'Validation error in $dstRel' ); + } + $dstPath = "$root/$dstRel"; + $dstDir = dirname( $dstPath ); + + if ( !is_dir( $dstDir ) ) { + if ( !wfMkdirParents( $dstDir ) ) { + return $this->newFatal( 'directorycreateerror', $dstDir ); + } + // In the deleted zone, seed new directories with a blank + // index.html, to prevent crawling + if ( $dstZone == 'deleted' ) { + file_put_contents( "$dstDir/index.html", '' ); + } + } + + if ( self::isVirtualUrl( $srcPath ) ) { + $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath ); + } + if ( !is_file( $srcPath ) ) { + // Make a list of files that don't exist for return to the caller + $status->fatal( 'filenotfound', $srcPath ); + continue; + } + if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) { + if ( $flags & self::OVERWRITE_SAME ) { + $hashSource = sha1_file( $srcPath ); + $hashDest = sha1_file( $dstPath ); + if ( $hashSource != $hashDest ) { + $status->fatal( 'fileexistserror', $dstPath ); + } + } else { + $status->fatal( 'fileexistserror', $dstPath ); + } + } + } + + $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE ); + + // Abort now on failure + if ( !$status->ok ) { + return $status; + } + + foreach ( $triplets as $triplet ) { + list( $srcPath, $dstZone, $dstRel ) = $triplet; + $root = $this->getZonePath( $dstZone ); + $dstPath = "$root/$dstRel"; + $good = true; + + if ( $flags & self::DELETE_SOURCE ) { + if ( $deleteDest ) { + unlink( $dstPath ); + } + if ( !rename( $srcPath, $dstPath ) ) { + $status->error( 'filerenameerror', $srcPath, $dstPath ); + $good = false; + } + } else { + if ( !copy( $srcPath, $dstPath ) ) { + $status->error( 'filecopyerror', $srcPath, $dstPath ); + $good = false; + } + } + if ( $good ) { + chmod( $dstPath, 0644 ); + $status->successCount++; + } else { + $status->failCount++; + } + } + return $status; + } + + /** + * Pick a random name in the temp zone and store a file to it. + * @param string $originalName The base name of the file as specified + * by the user. The file extension will be maintained. + * @param string $srcPath The current location of the file. + * @return FileRepoStatus object with the URL in the value. + */ + function storeTemp( $originalName, $srcPath ) { + $date = gmdate( "YmdHis" ); + $hashPath = $this->getHashPath( $originalName ); + $dstRel = "$hashPath$date!$originalName"; + $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); + + $result = $this->store( $srcPath, 'temp', $dstRel ); + $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; + return $result; + } + + /** + * Remove a temporary file or mark it for garbage collection + * @param string $virtualUrl The virtual URL returned by storeTemp + * @return boolean True on success, false on failure + */ + function freeTemp( $virtualUrl ) { + $temp = "mwrepo://{$this->name}/temp"; + if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { + wfDebug( __METHOD__.": Invalid virtual URL\n" ); + return false; + } + $path = $this->resolveVirtualUrl( $virtualUrl ); + wfSuppressWarnings(); + $success = unlink( $path ); + wfRestoreWarnings(); + return $success; + } + + /** + * Publish a batch of files + * @param array $triplets (source,dest,archive) triplets as per publish() + * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source files should be deleted if possible + */ + function publishBatch( $triplets, $flags = 0 ) { + // Perform initial checks + if ( !is_writable( $this->directory ) ) { + return $this->newFatal( 'upload_directory_read_only', $this->directory ); + } + $status = $this->newGood( array() ); + foreach ( $triplets as $i => $triplet ) { + list( $srcPath, $dstRel, $archiveRel ) = $triplet; + + if ( substr( $srcPath, 0, 9 ) == 'mwrepo://' ) { + $triplets[$i][0] = $srcPath = $this->resolveVirtualUrl( $srcPath ); + } + if ( !$this->validateFilename( $dstRel ) ) { + throw new MWException( 'Validation error in $dstRel' ); + } + if ( !$this->validateFilename( $archiveRel ) ) { + throw new MWException( 'Validation error in $archiveRel' ); + } + $dstPath = "{$this->directory}/$dstRel"; + $archivePath = "{$this->directory}/$archiveRel"; + + $dstDir = dirname( $dstPath ); + $archiveDir = dirname( $archivePath ); + // Abort immediately on directory creation errors since they're likely to be repetitive + if ( !is_dir( $dstDir ) && !wfMkdirParents( $dstDir ) ) { + return $this->newFatal( 'directorycreateerror', $dstDir ); + } + if ( !is_dir( $archiveDir ) && !wfMkdirParents( $archiveDir ) ) { + return $this->newFatal( 'directorycreateerror', $archiveDir ); + } + if ( !is_file( $srcPath ) ) { + // Make a list of files that don't exist for return to the caller + $status->fatal( 'filenotfound', $srcPath ); + } + } + + if ( !$status->ok ) { + return $status; + } + + foreach ( $triplets as $i => $triplet ) { + list( $srcPath, $dstRel, $archiveRel ) = $triplet; + $dstPath = "{$this->directory}/$dstRel"; + $archivePath = "{$this->directory}/$archiveRel"; + + // Archive destination file if it exists + if( is_file( $dstPath ) ) { + // Check if the archive file exists + // This is a sanity check to avoid data loss. In UNIX, the rename primitive + // unlinks the destination file if it exists. DB-based synchronisation in + // publishBatch's caller should prevent races. In Windows there's no + // problem because the rename primitive fails if the destination exists. + if ( is_file( $archivePath ) ) { + $success = false; + } else { + wfSuppressWarnings(); + $success = rename( $dstPath, $archivePath ); + wfRestoreWarnings(); + } + + if( !$success ) { + $status->error( 'filerenameerror',$dstPath, $archivePath ); + $status->failCount++; + continue; + } else { + wfDebug(__METHOD__.": moved file $dstPath to $archivePath\n"); + } + $status->value[$i] = 'archived'; + } else { + $status->value[$i] = 'new'; + } + + $good = true; + wfSuppressWarnings(); + if ( $flags & self::DELETE_SOURCE ) { + if ( !rename( $srcPath, $dstPath ) ) { + $status->error( 'filerenameerror', $srcPath, $dstPath ); + $good = false; + } + } else { + if ( !copy( $srcPath, $dstPath ) ) { + $status->error( 'filecopyerror', $srcPath, $dstPath ); + $good = false; + } + } + wfRestoreWarnings(); + + if ( $good ) { + $status->successCount++; + wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n"); + // Thread-safe override for umask + chmod( $dstPath, 0644 ); + } else { + $status->failCount++; + } + } + return $status; + } + + /** + * Move a group of files to the deletion archive. + * If no valid deletion archive is configured, this may either delete the + * file or throw an exception, depending on the preference of the repository. + * + * @param array $sourceDestPairs Array of source/destination pairs. Each element + * is a two-element array containing the source file path relative to the + * public root in the first element, and the archive file path relative + * to the deleted zone root in the second element. + * @return FileRepoStatus + */ + function deleteBatch( $sourceDestPairs ) { + $status = $this->newGood(); + if ( !$this->deletedDir ) { + throw new MWException( __METHOD__.': no valid deletion archive directory' ); + } + + /** + * Validate filenames and create archive directories + */ + foreach ( $sourceDestPairs as $pair ) { + list( $srcRel, $archiveRel ) = $pair; + if ( !$this->validateFilename( $srcRel ) ) { + throw new MWException( __METHOD__.':Validation error in $srcRel' ); + } + if ( !$this->validateFilename( $archiveRel ) ) { + throw new MWException( __METHOD__.':Validation error in $archiveRel' ); + } + $archivePath = "{$this->deletedDir}/$archiveRel"; + $archiveDir = dirname( $archivePath ); + if ( !is_dir( $archiveDir ) ) { + if ( !wfMkdirParents( $archiveDir ) ) { + $status->fatal( 'directorycreateerror', $archiveDir ); + continue; + } + // Seed new directories with a blank index.html, to prevent crawling + file_put_contents( "$archiveDir/index.html", '' ); + } + // Check if the archive directory is writable + // This doesn't appear to work on NTFS + if ( !is_writable( $archiveDir ) ) { + $status->fatal( 'filedelete-archive-read-only', $archiveDir ); + } + } + if ( !$status->ok ) { + // Abort early + return $status; + } + + /** + * Move the files + * We're now committed to returning an OK result, which will lead to + * the files being moved in the DB also. + */ + foreach ( $sourceDestPairs as $pair ) { + list( $srcRel, $archiveRel ) = $pair; + $srcPath = "{$this->directory}/$srcRel"; + $archivePath = "{$this->deletedDir}/$archiveRel"; + $good = true; + if ( file_exists( $archivePath ) ) { + # A file with this content hash is already archived + if ( !@unlink( $srcPath ) ) { + $status->error( 'filedeleteerror', $srcPath ); + $good = false; + } + } else{ + if ( !@rename( $srcPath, $archivePath ) ) { + $status->error( 'filerenameerror', $srcPath, $archivePath ); + $good = false; + } else { + chmod( $archivePath, 0644 ); + } + } + if ( $good ) { + $status->successCount++; + } else { + $status->failCount++; + } + } + return $status; + } + + /** + * Get a relative path including trailing slash, e.g. f/fa/ + * If the repo is not hashed, returns an empty string + */ + function getHashPath( $name ) { + return FileRepo::getHashPathForLevel( $name, $this->hashLevels ); + } + + /** + * Get a relative path for a deletion archive key, + * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg + */ + function getDeletedHashPath( $key ) { + $path = ''; + for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) { + $path .= $key[$i] . '/'; + } + return $path; + } + + /** + * Call a callback function for every file in the repository. + * Uses the filesystem even in child classes. + */ + function enumFilesInFS( $callback ) { + $numDirs = 1 << ( $this->hashLevels * 4 ); + for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) { + $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex ); + $path = $this->directory; + for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) { + $path .= '/' . substr( $hexString, 0, $hexPos + 1 ); + } + if ( !file_exists( $path ) || !is_dir( $path ) ) { + continue; + } + $dir = opendir( $path ); + while ( false !== ( $name = readdir( $dir ) ) ) { + call_user_func( $callback, $path . '/' . $name ); + } + } + } + + /** + * Call a callback function for every file in the repository + * May use either the database or the filesystem + */ + function enumFiles( $callback ) { + $this->enumFilesInFS( $callback ); + } + + /** + * Get properties of a file with a given virtual URL + * The virtual URL must refer to this repo + */ + function getFileProps( $virtualUrl ) { + $path = $this->resolveVirtualUrl( $virtualUrl ); + return File::getPropsFromPath( $path ); + } + + /** + * Path disclosure protection functions + * + * Get a callback function to use for cleaning error message parameters + */ + function getErrorCleanupFunction() { + switch ( $this->pathDisclosureProtection ) { + case 'simple': + $callback = array( $this, 'simpleClean' ); + break; + default: + $callback = parent::getErrorCleanupFunction(); + } + return $callback; + } + + function simpleClean( $param ) { + if ( !isset( $this->simpleCleanPairs ) ) { + global $IP; + $this->simpleCleanPairs = array( + $this->directory => 'public', + "{$this->directory}/temp" => 'temp', + $IP => '$IP', + dirname( __FILE__ ) => '$IP/extensions/WebStore', + ); + if ( $this->deletedDir ) { + $this->simpleCleanPairs[$this->deletedDir] = 'deleted'; + } + } + return strtr( $param, $this->simpleCleanPairs ); + } + +} + + diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php new file mode 100644 index 00000000..21b7a865 --- /dev/null +++ b/includes/filerepo/File.php @@ -0,0 +1,1133 @@ +getLocalRepo()->newFile($title); + * + * The convenience functions wfLocalFile() and wfFindFile() should be sufficient + * in most cases. + * + * @addtogroup FileRepo + */ +abstract class File { + const DELETED_FILE = 1; + const DELETED_COMMENT = 2; + const DELETED_USER = 4; + const DELETED_RESTRICTED = 8; + const RENDER_NOW = 1; + + const DELETE_SOURCE = 1; + + /** + * Some member variables can be lazy-initialised using __get(). The + * initialisation function for these variables is always a function named + * like getVar(), where Var is the variable name with upper-case first + * letter. + * + * The following variables are initialised in this way in this base class: + * name, extension, handler, path, canRender, isSafeFile, + * transformScript, hashPath, pageCount, url + * + * Code within this class should generally use the accessor function + * directly, since __get() isn't re-entrant and therefore causes bugs that + * depend on initialisation order. + */ + + /** + * The following member variables are not lazy-initialised + */ + var $repo, $title, $lastError; + + /** + * Call this constructor from child classes + */ + function __construct( $title, $repo ) { + $this->title = $title; + $this->repo = $repo; + } + + function __get( $name ) { + $function = array( $this, 'get' . ucfirst( $name ) ); + if ( !is_callable( $function ) ) { + return null; + } else { + $this->$name = call_user_func( $function ); + return $this->$name; + } + } + + /** + * Normalize a file extension to the common form, and ensure it's clean. + * Extensions with non-alphanumeric characters will be discarded. + * + * @param $ext string (without the .) + * @return string + */ + static function normalizeExtension( $ext ) { + $lower = strtolower( $ext ); + $squish = array( + 'htm' => 'html', + 'jpeg' => 'jpg', + 'mpeg' => 'mpg', + 'tiff' => 'tif' ); + if( isset( $squish[$lower] ) ) { + return $squish[$lower]; + } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) { + return $lower; + } else { + return ''; + } + } + + /** + * Upgrade the database row if there is one + * Called by ImagePage + * STUB + */ + function upgradeRow() {} + + /** + * Split an internet media type into its two components; if not + * a two-part name, set the minor type to 'unknown'. + * + * @param $mime "text/html" etc + * @return array ("text", "html") etc + */ + static function splitMime( $mime ) { + if( strpos( $mime, '/' ) !== false ) { + return explode( '/', $mime, 2 ); + } else { + return array( $mime, 'unknown' ); + } + } + + /** + * Return the name of this file + */ + public function getName() { + if ( !isset( $this->name ) ) { + $this->name = $this->repo->getNameFromTitle( $this->title ); + } + return $this->name; + } + + /** + * Get the file extension, e.g. "svg" + */ + function getExtension() { + if ( !isset( $this->extension ) ) { + $n = strrpos( $this->getName(), '.' ); + $this->extension = self::normalizeExtension( + $n ? substr( $this->getName(), $n + 1 ) : '' ); + } + return $this->extension; + } + + /** + * Return the associated title object + * @public + */ + function getTitle() { return $this->title; } + + /** + * Return the URL of the file + * @public + */ + function getUrl() { + if ( !isset( $this->url ) ) { + $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel(); + } + return $this->url; + } + + function getViewURL() { + if( $this->mustRender()) { + if( $this->canRender() ) { + return $this->createThumb( $this->getWidth() ); + } + else { + wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n"); + return $this->getURL(); #hm... return NULL? + } + } else { + return $this->getURL(); + } + } + + /** + * Return the full filesystem path to the file. Note that this does + * not mean that a file actually exists under that location. + * + * This path depends on whether directory hashing is active or not, + * i.e. whether the files are all found in the same directory, + * or in hashed paths like /images/3/3c. + * + * May return false if the file is not locally accessible. + * + * @public + */ + function getPath() { + if ( !isset( $this->path ) ) { + $this->path = $this->repo->getZonePath('public') . '/' . $this->getRel(); + } + return $this->path; + } + + /** + * Alias for getPath() + * @public + */ + function getFullPath() { + return $this->getPath(); + } + + /** + * Return the width of the image. Returns false if the width is unknown + * or undefined. + * + * STUB + * Overridden by LocalFile, UnregisteredLocalFile + */ + public function getWidth( $page = 1 ) { return false; } + + /** + * Return the height of the image. Returns false if the height is unknown + * or undefined + * + * STUB + * Overridden by LocalFile, UnregisteredLocalFile + */ + public function getHeight( $page = 1 ) { return false; } + + /** + * Get the duration of a media file in seconds + */ + public function getLength() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->getLength( $this ); + } else { + return 0; + } + } + + /** + * Get handler-specific metadata + * Overridden by LocalFile, UnregisteredLocalFile + * STUB + */ + function getMetadata() { return false; } + + /** + * Return the size of the image file, in bytes + * Overridden by LocalFile, UnregisteredLocalFile + * STUB + */ + public function getSize() { return false; } + + /** + * Returns the mime type of the file. + * Overridden by LocalFile, UnregisteredLocalFile + * STUB + */ + function getMimeType() { return 'unknown/unknown'; } + + /** + * Return the type of the media in the file. + * Use the value returned by this function with the MEDIATYPE_xxx constants. + * Overridden by LocalFile, + * STUB + */ + function getMediaType() { return MEDIATYPE_UNKNOWN; } + + /** + * Checks if the output of transform() for this file is likely + * to be valid. If this is false, various user elements will + * display a placeholder instead. + * + * Currently, this checks if the file is an image format + * that can be converted to a format + * supported by all browsers (namely GIF, PNG and JPEG), + * or if it is an SVG image and SVG conversion is enabled. + */ + function canRender() { + if ( !isset( $this->canRender ) ) { + $this->canRender = $this->getHandler() && $this->handler->canRender( $this ); + } + return $this->canRender; + } + + /** + * Accessor for __get() + */ + protected function getCanRender() { + return $this->canRender(); + } + + /** + * Return true if the file is of a type that can't be directly + * rendered by typical browsers and needs to be re-rasterized. + * + * This returns true for everything but the bitmap types + * supported by all browsers, i.e. JPEG; GIF and PNG. It will + * also return true for any non-image formats. + * + * @return bool + */ + function mustRender() { + return $this->getHandler() && $this->handler->mustRender( $this ); + } + + /** + * Alias for canRender() + */ + function allowInlineDisplay() { + return $this->canRender(); + } + + /** + * Determines if this media file is in a format that is unlikely to + * contain viruses or malicious content. It uses the global + * $wgTrustedMediaFormats list to determine if the file is safe. + * + * This is used to show a warning on the description page of non-safe files. + * It may also be used to disallow direct [[media:...]] links to such files. + * + * Note that this function will always return true if allowInlineDisplay() + * or isTrustedFile() is true for this file. + */ + function isSafeFile() { + if ( !isset( $this->isSafeFile ) ) { + $this->isSafeFile = $this->_getIsSafeFile(); + } + return $this->isSafeFile; + } + + /** Accessor for __get() */ + protected function getIsSafeFile() { + return $this->isSafeFile(); + } + + /** Uncached accessor */ + protected function _getIsSafeFile() { + if ($this->allowInlineDisplay()) return true; + if ($this->isTrustedFile()) return true; + + global $wgTrustedMediaFormats; + + $type= $this->getMediaType(); + $mime= $this->getMimeType(); + #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n"); + + if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted + if ( in_array( $type, $wgTrustedMediaFormats) ) return true; + + if ($mime==="unknown/unknown") return false; #unknown type, not trusted + if ( in_array( $mime, $wgTrustedMediaFormats) ) return true; + + return false; + } + + /** Returns true if the file is flagged as trusted. Files flagged that way + * can be linked to directly, even if that is not allowed for this type of + * file normally. + * + * This is a dummy function right now and always returns false. It could be + * implemented to extract a flag from the database. The trusted flag could be + * set on upload, if the user has sufficient privileges, to bypass script- + * and html-filters. It may even be coupled with cryptographics signatures + * or such. + */ + function isTrustedFile() { + #this could be implemented to check a flag in the databas, + #look for signatures, etc + return false; + } + + /** + * Returns true if file exists in the repository. + * + * Overridden by LocalFile to avoid unnecessary stat calls. + * + * @return boolean Whether file exists in the repository. + */ + public function exists() { + return $this->getPath() && file_exists( $this->path ); + } + + function getTransformScript() { + if ( !isset( $this->transformScript ) ) { + $this->transformScript = false; + if ( $this->repo ) { + $script = $this->repo->getThumbScriptUrl(); + if ( $script ) { + $this->transformScript = "$script?f=" . urlencode( $this->getName() ); + } + } + } + return $this->transformScript; + } + + /** + * Get a ThumbnailImage which is the same size as the source + */ + function getUnscaledThumb( $page = false ) { + $width = $this->getWidth( $page ); + if ( !$width ) { + return $this->iconThumb(); + } + if ( $page ) { + $params = array( + 'page' => $page, + 'width' => $this->getWidth( $page ) + ); + } else { + $params = array( 'width' => $this->getWidth() ); + } + return $this->transform( $params ); + } + + /** + * Return the file name of a thumbnail with the specified parameters + * + * @param array $params Handler-specific parameters + * @private -ish + */ + function thumbName( $params ) { + if ( !$this->getHandler() ) { + return null; + } + $extension = $this->getExtension(); + list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType() ); + $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName(); + if ( $thumbExt != $extension ) { + $thumbName .= ".$thumbExt"; + } + return $thumbName; + } + + /** + * Create a thumbnail of the image having the specified width/height. + * The thumbnail will not be created if the width is larger than the + * image's width. Let the browser do the scaling in this case. + * The thumbnail is stored on disk and is only computed if the thumbnail + * file does not exist OR if it is older than the image. + * Returns the URL. + * + * Keeps aspect ratio of original image. If both width and height are + * specified, the generated image will be no bigger than width x height, + * and will also have correct aspect ratio. + * + * @param integer $width maximum width of the generated thumbnail + * @param integer $height maximum height of the image (optional) + */ + public function createThumb( $width, $height = -1 ) { + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; + } + $thumb = $this->transform( $params ); + if( is_null( $thumb ) || $thumb->isError() ) return ''; + return $thumb->getUrl(); + } + + /** + * As createThumb, but returns a ThumbnailImage object. This can + * provide access to the actual file, the real size of the thumb, + * and can produce a convenient tag for you. + * + * For non-image formats, this may return a filetype-specific icon. + * + * @param integer $width maximum width of the generated thumbnail + * @param integer $height maximum height of the image (optional) + * @param boolean $render True to render the thumbnail if it doesn't exist, + * false to just return the URL + * + * @return ThumbnailImage or null on failure + * + * @deprecated use transform() + */ + public function getThumbnail( $width, $height=-1, $render = true ) { + $params = array( 'width' => $width ); + if ( $height != -1 ) { + $params['height'] = $height; + } + $flags = $render ? self::RENDER_NOW : 0; + return $this->transform( $params, $flags ); + } + + /** + * Transform a media file + * + * @param array $params An associative array of handler-specific parameters. Typical + * keys are width, height and page. + * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering + * @return MediaTransformOutput + */ + function transform( $params, $flags = 0 ) { + global $wgUseSquid, $wgIgnoreImageErrors; + + wfProfileIn( __METHOD__ ); + do { + if ( !$this->canRender() ) { + // not a bitmap or renderable image, don't try. + $thumb = $this->iconThumb(); + break; + } + + $script = $this->getTransformScript(); + if ( $script && !($flags & self::RENDER_NOW) ) { + // Use a script to transform on client request + $thumb = $this->handler->getScriptedTransform( $this, $script, $params ); + break; + } + + $normalisedParams = $params; + $this->handler->normaliseParams( $this, $normalisedParams ); + $thumbName = $this->thumbName( $normalisedParams ); + $thumbPath = $this->getThumbPath( $thumbName ); + $thumbUrl = $this->getThumbUrl( $thumbName ); + + if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } + + wfDebug( __METHOD__.": Doing stat for $thumbPath\n" ); + $this->migrateThumbFile( $thumbName ); + if ( file_exists( $thumbPath ) ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + break; + } + $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params ); + + // Ignore errors if requested + if ( !$thumb ) { + $thumb = null; + } elseif ( $thumb->isError() ) { + $this->lastError = $thumb->toText(); + if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); + } + } + + if ( $wgUseSquid ) { + wfPurgeSquidServers( array( $thumbUrl ) ); + } + } while (false); + + wfProfileOut( __METHOD__ ); + return $thumb; + } + + /** + * Hook into transform() to allow migration of thumbnail files + * STUB + * Overridden by LocalFile + */ + function migrateThumbFile( $thumbName ) {} + + /** + * Get a MediaHandler instance for this file + */ + function getHandler() { + if ( !isset( $this->handler ) ) { + $this->handler = MediaHandler::getHandler( $this->getMimeType() ); + } + return $this->handler; + } + + /** + * Get a ThumbnailImage representing a file type icon + * @return ThumbnailImage + */ + function iconThumb() { + global $wgStylePath, $wgStyleDirectory; + + $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' ); + foreach( $try as $icon ) { + $path = '/common/images/icons/' . $icon; + $filepath = $wgStyleDirectory . $path; + if( file_exists( $filepath ) ) { + return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 ); + } + } + return null; + } + + /** + * Get last thumbnailing error. + * Largely obsolete. + */ + function getLastError() { + return $this->lastError; + } + + /** + * Get all thumbnail names previously generated for this file + * STUB + * Overridden by LocalFile + */ + function getThumbnails() { return array(); } + + /** + * Purge shared caches such as thumbnails and DB data caching + * STUB + * Overridden by LocalFile + */ + function purgeCache( $archiveFiles = array() ) {} + + /** + * Purge the file description page, but don't go after + * pages using the file. Use when modifying file history + * but not the current data. + */ + function purgeDescription() { + $title = $this->getTitle(); + if ( $title ) { + $title->invalidateCache(); + $title->purgeSquid(); + } + } + + /** + * Purge metadata and all affected pages when the file is created, + * deleted, or majorly updated. + */ + function purgeEverything() { + // Delete thumbnails and refresh file metadata cache + $this->purgeCache(); + $this->purgeDescription(); + + // Purge cache of all pages using this file + $title = $this->getTitle(); + if ( $title ) { + $update = new HTMLCacheUpdate( $title, 'imagelinks' ); + $update->doUpdate(); + } + } + + /** + * Return the history of this file, line by line. Starts with current version, + * then old versions. Should return an object similar to an image/oldimage + * database row. + * + * STUB + * Overridden in LocalFile + */ + public function nextHistoryLine() { + return false; + } + + /** + * Reset the history pointer to the first element of the history. + * Always call this function after using nextHistoryLine() to free db resources + * STUB + * Overridden in LocalFile. + */ + public function resetHistory() {} + + /** + * Get the filename hash component of the directory including trailing slash, + * e.g. f/fa/ + * If the repository is not hashed, returns an empty string. + */ + function getHashPath() { + if ( !isset( $this->hashPath ) ) { + $this->hashPath = $this->repo->getHashPath( $this->getName() ); + } + return $this->hashPath; + } + + /** + * Get the path of the file relative to the public zone root + */ + function getRel() { + return $this->getHashPath() . $this->getName(); + } + + /** + * Get urlencoded relative path of the file + */ + function getUrlRel() { + return $this->getHashPath() . rawurlencode( $this->getName() ); + } + + /** Get the relative path for an archive file */ + function getArchiveRel( $suffix = false ) { + $path = 'archive/' . $this->getHashPath(); + if ( $suffix === false ) { + $path = substr( $path, 0, -1 ); + } else { + $path .= $suffix; + } + return $path; + } + + /** Get relative path for a thumbnail file */ + function getThumbRel( $suffix = false ) { + $path = 'thumb/' . $this->getRel(); + if ( $suffix !== false ) { + $path .= '/' . $suffix; + } + return $path; + } + + /** Get the path of the archive directory, or a particular file if $suffix is specified */ + function getArchivePath( $suffix = false ) { + return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel(); + } + + /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ + function getThumbPath( $suffix = false ) { + return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix ); + } + + /** Get the URL of the archive directory, or a particular file if $suffix is specified */ + function getArchiveUrl( $suffix = false ) { + $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath(); + if ( $suffix === false ) { + $path = substr( $path, 0, -1 ); + } else { + $path .= rawurlencode( $suffix ); + } + return $path; + } + + /** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */ + function getThumbUrl( $suffix = false ) { + $path = $this->repo->getZoneUrl('public') . '/thumb/' . $this->getUrlRel(); + if ( $suffix !== false ) { + $path .= '/' . rawurlencode( $suffix ); + } + return $path; + } + + /** Get the virtual URL for an archive file or directory */ + function getArchiveVirtualUrl( $suffix = false ) { + $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath(); + if ( $suffix === false ) { + $path = substr( $path, 0, -1 ); + } else { + $path .= rawurlencode( $suffix ); + } + return $path; + } + + /** Get the virtual URL for a thumbnail file or directory */ + function getThumbVirtualUrl( $suffix = false ) { + $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getUrlRel(); + if ( $suffix !== false ) { + $path .= '/' . rawurlencode( $suffix ); + } + return $path; + } + + /** Get the virtual URL for the file itself */ + function getVirtualUrl( $suffix = false ) { + $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel(); + if ( $suffix !== false ) { + $path .= '/' . rawurlencode( $suffix ); + } + return $path; + } + + /** + * @return bool + */ + function isHashed() { + return $this->repo->isHashed(); + } + + function readOnlyError() { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } + + /** + * Record a file upload in the upload log and the image table + * STUB + * Overridden by LocalFile + */ + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { + $this->readOnlyError(); + } + + /** + * Move or copy a file to its public location. If a file exists at the + * destination, move it to an archive. Returns the archive name on success + * or an empty string if it was a new file, and a wikitext-formatted + * WikiError object on failure. + * + * The archive name should be passed through to recordUpload for database + * registration. + * + * @param string $sourcePath Local filesystem path to the source image + * @param integer $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move + * rather than copy + * @return The archive name on success or an empty string if it was a new + * file, and a wikitext-formatted WikiError object on failure. + * + * STUB + * Overridden by LocalFile + */ + function publish( $srcPath, $flags = 0 ) { + $this->readOnlyError(); + } + + /** + * Get an array of Title objects which are articles which use this file + * Also adds their IDs to the link cache + * + * This is mostly copied from Title::getLinksTo() + * + * @deprecated Use HTMLCacheUpdate, this function uses too much memory + */ + function getLinksTo( $options = '' ) { + wfProfileIn( __METHOD__ ); + + // Note: use local DB not repo DB, we want to know local links + if ( $options ) { + $db = wfGetDB( DB_MASTER ); + } else { + $db = wfGetDB( DB_SLAVE ); + } + $linkCache =& LinkCache::singleton(); + + list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' ); + $encName = $db->addQuotes( $this->getName() ); + $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options"; + $res = $db->query( $sql, __METHOD__ ); + + $retVal = array(); + if ( $db->numRows( $res ) ) { + while ( $row = $db->fetchObject( $res ) ) { + if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) { + $linkCache->addGoodLinkObj( $row->page_id, $titleObj ); + $retVal[] = $titleObj; + } + } + } + $db->freeResult( $res ); + wfProfileOut( __METHOD__ ); + return $retVal; + } + + function formatMetadata() { + if ( !$this->getHandler() ) { + return false; + } + return $this->getHandler()->formatMetadata( $this, $this->getMetadata() ); + } + + /** + * Returns true if the file comes from the local file repository. + * + * @return bool + */ + function isLocal() { + return $this->getRepoName() == 'local'; + } + + /** + * Returns the name of the repository. + * + * @return string + */ + function getRepoName() { + return $this->repo ? $this->repo->getName() : 'unknown'; + } + + /** + * Returns true if the image is an old version + * STUB + */ + function isOld() { + return false; + } + + /** + * Is this file a "deleted" file in a private archive? + * STUB + */ + function isDeleted( $field ) { + return false; + } + + /** + * Was this file ever deleted from the wiki? + * + * @return bool + */ + function wasDeleted() { + $title = $this->getTitle(); + return $title && $title->isDeleted() > 0; + } + + /** + * Delete all versions of the file. + * + * Moves the files into an archive directory (or deletes them) + * and removes the database rows. + * + * Cache purging is done; logging is caller's responsibility. + * + * @param $reason + * @return true on success, false on some kind of failure + * STUB + * Overridden by LocalFile + */ + function delete( $reason, $suppress=false ) { + $this->readOnlyError(); + } + + /** + * Restore all or specified deleted revisions to the given file. + * Permissions and logging are left to the caller. + * + * May throw database exceptions on error. + * + * @param $versions set of record ids of deleted items to restore, + * or empty to restore all revisions. + * @return the number of file revisions restored if successful, + * or false on failure + * STUB + * Overridden by LocalFile + */ + function restore( $versions=array(), $Unsuppress=false ) { + $this->readOnlyError(); + } + + /** + * Returns 'true' if this image is a multipage document, e.g. a DJVU + * document. + * + * @return Bool + */ + function isMultipage() { + return $this->getHandler() && $this->handler->isMultiPage( $this ); + } + + /** + * Returns the number of pages of a multipage document, or NULL for + * documents which aren't multipage documents + */ + function pageCount() { + if ( !isset( $this->pageCount ) ) { + if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) { + $this->pageCount = $this->handler->pageCount( $this ); + } else { + $this->pageCount = false; + } + } + return $this->pageCount; + } + + /** + * Calculate the height of a thumbnail using the source and destination width + */ + static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) { + // Exact integer multiply followed by division + if ( $srcWidth == 0 ) { + return 0; + } else { + return round( $srcHeight * $dstWidth / $srcWidth ); + } + } + + /** + * Get an image size array like that returned by getimagesize(), or false if it + * can't be determined. + * + * @param string $fileName The filename + * @return array + */ + function getImageSize( $fileName ) { + if ( !$this->getHandler() ) { + return false; + } + return $this->handler->getImageSize( $this, $fileName ); + } + + /** + * Get the URL of the image description page. May return false if it is + * unknown or not applicable. + */ + function getDescriptionUrl() { + return $this->repo->getDescriptionUrl( $this->getName() ); + } + + /** + * Get the HTML text of the description page, if available + */ + function getDescriptionText() { + if ( !$this->repo->fetchDescription ) { + return false; + } + $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName() ); + if ( $renderUrl ) { + wfDebug( "Fetching shared description from $renderUrl\n" ); + return Http::get( $renderUrl ); + } else { + return false; + } + } + + /** + * Get the 14-character timestamp of the file upload, or false if + * it doesn't exist + */ + function getTimestamp() { + $path = $this->getPath(); + if ( !file_exists( $path ) ) { + return false; + } + return wfTimestamp( filemtime( $path ) ); + } + + /** + * Get the SHA-1 base 36 hash of the file + */ + function getSha1() { + return self::sha1Base36( $this->getPath() ); + } + + /** + * Determine if the current user is allowed to view a particular + * field of this file, if it's marked as deleted. + * STUB + * @param int $field + * @return bool + */ + function userCan( $field ) { + return true; + } + + /** + * Get an associative array containing information about a file in the local filesystem\ + * + * @param string $path Absolute local filesystem path + * @param mixed $ext The file extension, or true to extract it from the filename. + * Set it to false to ignore the extension. + */ + static function getPropsFromPath( $path, $ext = true ) { + wfProfileIn( __METHOD__ ); + wfDebug( __METHOD__.": Getting file info for $path\n" ); + $info = array( + 'fileExists' => file_exists( $path ) && !is_dir( $path ) + ); + $gis = false; + if ( $info['fileExists'] ) { + $magic = MimeMagic::singleton(); + + $info['mime'] = $magic->guessMimeType( $path, $ext ); + list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] ); + $info['media_type'] = $magic->getMediaType( $path, $info['mime'] ); + + # Get size in bytes + $info['size'] = filesize( $path ); + + # Height, width and metadata + $handler = MediaHandler::getHandler( $info['mime'] ); + if ( $handler ) { + $tempImage = (object)array(); + $info['metadata'] = $handler->getMetadata( $tempImage, $path ); + $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] ); + } else { + $gis = false; + $info['metadata'] = ''; + } + $info['sha1'] = self::sha1Base36( $path ); + + wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n"); + } else { + $info['mime'] = NULL; + $info['media_type'] = MEDIATYPE_UNKNOWN; + $info['metadata'] = ''; + $info['sha1'] = ''; + wfDebug(__METHOD__.": $path NOT FOUND!\n"); + } + if( $gis ) { + # NOTE: $gis[2] contains a code for the image type. This is no longer used. + $info['width'] = $gis[0]; + $info['height'] = $gis[1]; + if ( isset( $gis['bits'] ) ) { + $info['bits'] = $gis['bits']; + } else { + $info['bits'] = 0; + } + } else { + $info['width'] = 0; + $info['height'] = 0; + $info['bits'] = 0; + } + wfProfileOut( __METHOD__ ); + return $info; + } + + /** + * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case + * encoding, zero padded to 31 digits. + * + * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36 + * fairly neatly. + * + * Returns false on failure + */ + static function sha1Base36( $path ) { + wfSuppressWarnings(); + $hash = sha1_file( $path ); + wfRestoreWarnings(); + if ( $hash === false ) { + return false; + } else { + return wfBaseConvert( $hash, 16, 36, 31 ); + } + } + + function getLongDesc() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->getLongDesc( $this ); + } else { + return MediaHandler::getLongDesc( $this ); + } + } + + function getShortDesc() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->getShortDesc( $this ); + } else { + return MediaHandler::getShortDesc( $this ); + } + } + + function getDimensionsString() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->getDimensionsString( $this ); + } else { + return ''; + } + } +} +/** + * Aliases for backwards compatibility with 1.6 + */ +define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE ); +define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT ); +define( 'MW_IMG_DELETED_USER', File::DELETED_USER ); +define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED ); + + diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php new file mode 100644 index 00000000..cf6d65c2 --- /dev/null +++ b/includes/filerepo/FileRepo.php @@ -0,0 +1,404 @@ +name = $info['name']; + + // Optional settings + $this->initialCapital = true; // by default + foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', + 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var ) + { + if ( isset( $info[$var] ) ) { + $this->$var = $info[$var]; + } + } + $this->transformVia404 = !empty( $info['transformVia404'] ); + } + + /** + * Determine if a string is an mwrepo:// URL + */ + static function isVirtualUrl( $url ) { + return substr( $url, 0, 9 ) == 'mwrepo://'; + } + + /** + * Create a new File object from the local repository + * @param mixed $title Title object or string + * @param mixed $time Time at which the image is supposed to have existed. + * If this is specified, the returned object will be an + * instance of the repository's old file class instead of + * a current file. Repositories not supporting version + * control should return false if this parameter is set. + */ + function newFile( $title, $time = false ) { + if ( !($title instanceof Title) ) { + $title = Title::makeTitleSafe( NS_IMAGE, $title ); + if ( !is_object( $title ) ) { + return null; + } + } + if ( $time ) { + if ( $this->oldFileFactory ) { + return call_user_func( $this->oldFileFactory, $title, $this, $time ); + } else { + return false; + } + } else { + return call_user_func( $this->fileFactory, $title, $this ); + } + } + + /** + * Find an instance of the named file that existed at the specified time + * Returns false if the file did not exist. Repositories not supporting + * version control should return false if the time is specified. + * + * @param mixed $time 14-character timestamp, or false for the current version + */ + function findFile( $title, $time = false ) { + # First try the current version of the file to see if it precedes the timestamp + $img = $this->newFile( $title ); + if ( !$img ) { + return false; + } + if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) { + return $img; + } + # Now try an old version of the file + $img = $this->newFile( $title, $time ); + if ( $img->exists() ) { + return $img; + } + } + + /** + * Get the URL of thumb.php + */ + function getThumbScriptUrl() { + return $this->thumbScriptUrl; + } + + /** + * Returns true if the repository can transform files via a 404 handler + */ + function canTransformVia404() { + return $this->transformVia404; + } + + /** + * Get the name of an image from its title object + */ + function getNameFromTitle( $title ) { + global $wgCapitalLinks; + if ( $this->initialCapital != $wgCapitalLinks ) { + global $wgContLang; + $name = $title->getUserCaseDBKey(); + if ( $this->initialCapital ) { + $name = $wgContLang->ucfirst( $name ); + } + } else { + $name = $title->getDBkey(); + } + return $name; + } + + static function getHashPathForLevel( $name, $levels ) { + if ( $levels == 0 ) { + return ''; + } else { + $hash = md5( $name ); + $path = ''; + for ( $i = 1; $i <= $levels; $i++ ) { + $path .= substr( $hash, 0, $i ) . '/'; + } + return $path; + } + } + + /** + * Get the name of this repository, as specified by $info['name]' to the constructor + */ + function getName() { + return $this->name; + } + + /** + * Get the file description page base URL, or false if there isn't one. + * @private + */ + function getDescBaseUrl() { + if ( is_null( $this->descBaseUrl ) ) { + if ( !is_null( $this->articleUrl ) ) { + $this->descBaseUrl = str_replace( '$1', + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); + } elseif ( !is_null( $this->scriptDirUrl ) ) { + $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':'; + } else { + $this->descBaseUrl = false; + } + } + return $this->descBaseUrl; + } + + /** + * Get the URL of an image description page. May return false if it is + * unknown or not applicable. In general this should only be called by the + * File class, since it may return invalid results for certain kinds of + * repositories. Use File::getDescriptionUrl() in user code. + * + * In particular, it uses the article paths as specified to the repository + * constructor, whereas local repositories use the local Title functions. + */ + function getDescriptionUrl( $name ) { + $base = $this->getDescBaseUrl(); + if ( $base ) { + return $base . wfUrlencode( $name ); + } else { + return false; + } + } + + /** + * Get the URL of the content-only fragment of the description page. For + * MediaWiki this means action=render. This should only be called by the + * repository's file class, since it may return invalid results. User code + * should use File::getDescriptionText(). + */ + function getDescriptionRenderUrl( $name ) { + if ( isset( $this->scriptDirUrl ) ) { + return $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . + '&action=render'; + } else { + $descBase = $this->getDescBaseUrl(); + if ( $descBase ) { + return wfAppendQuery( $descBase . wfUrlencode( $name ), 'action=render' ); + } else { + return false; + } + } + } + + /** + * Store a file to a given destination. + * + * @param string $srcPath Source path or virtual URL + * @param string $dstZone Destination zone + * @param string $dstRel Destination relative path + * @param integer $flags Bitwise combination of the following flags: + * self::DELETE_SOURCE Delete the source file after upload + * self::OVERWRITE Overwrite an existing destination file instead of failing + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the + * same contents as the source + * @return FileRepoStatus + */ + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags ); + if ( $status->successCount == 0 ) { + $status->ok = false; + } + return $status; + } + + /** + * Store a batch of files + * + * @param array $triplets (src,zone,dest) triplets as per store() + * @param integer $flags Flags as per store + */ + abstract function storeBatch( $triplets, $flags = 0 ); + + /** + * Pick a random name in the temp zone and store a file to it. + * Returns a FileRepoStatus object with the URL in the value. + * + * @param string $originalName The base name of the file as specified + * by the user. The file extension will be maintained. + * @param string $srcPath The current location of the file. + */ + abstract function storeTemp( $originalName, $srcPath ); + + /** + * Remove a temporary file or mark it for garbage collection + * @param string $virtualUrl The virtual URL returned by storeTemp + * @return boolean True on success, false on failure + * STUB + */ + function freeTemp( $virtualUrl ) { + return true; + } + + /** + * Copy or move a file either from the local filesystem or from an mwrepo:// + * virtual URL, into this repository at the specified destination location. + * + * Returns a FileRepoStatus object. On success, the value contains "new" or + * "archived", to indicate whether the file was new with that name. + * + * @param string $srcPath The source path or URL + * @param string $dstRel The destination relative path + * @param string $archiveRel The relative path where the existing file is to + * be archived, if there is one. Relative to the public zone root. + * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source file should be deleted if possible + */ + function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { + $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags ); + if ( $status->successCount == 0 ) { + $status->ok = false; + } + if ( isset( $status->value[0] ) ) { + $status->value = $status->value[0]; + } else { + $status->value = false; + } + return $status; + } + + /** + * Publish a batch of files + * @param array $triplets (source,dest,archive) triplets as per publish() + * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source files should be deleted if possible + */ + abstract function publishBatch( $triplets, $flags = 0 ); + + /** + * Move a group of files to the deletion archive. + * + * If no valid deletion archive is configured, this may either delete the + * file or throw an exception, depending on the preference of the repository. + * + * The overwrite policy is determined by the repository -- currently FSRepo + * assumes a naming scheme in the deleted zone based on content hash, as + * opposed to the public zone which is assumed to be unique. + * + * @param array $sourceDestPairs Array of source/destination pairs. Each element + * is a two-element array containing the source file path relative to the + * public root in the first element, and the archive file path relative + * to the deleted zone root in the second element. + * @return FileRepoStatus + */ + abstract function deleteBatch( $sourceDestPairs ); + + /** + * Move a file to the deletion archive. + * If no valid deletion archive exists, this may either delete the file + * or throw an exception, depending on the preference of the repository + * @param mixed $srcRel Relative path for the file to be deleted + * @param mixed $archiveRel Relative path for the archive location. + * Relative to a private archive directory. + * @return WikiError object (wikitext-formatted), or true for success + */ + function delete( $srcRel, $archiveRel ) { + return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); + } + + /** + * Get properties of a file with a given virtual URL + * The virtual URL must refer to this repo + * Properties should ultimately be obtained via File::getPropsFromPath() + */ + abstract function getFileProps( $virtualUrl ); + + /** + * Call a callback function for every file in the repository + * May use either the database or the filesystem + * STUB + */ + function enumFiles( $callback ) { + throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) ); + } + + /** + * Determine if a relative path is valid, i.e. not blank or involving directory traveral + */ + function validateFilename( $filename ) { + if ( strval( $filename ) == '' ) { + return false; + } + if ( wfIsWindows() ) { + $filename = strtr( $filename, '\\', '/' ); + } + /** + * Use the same traversal protection as Title::secureAndSplit() + */ + if ( strpos( $filename, '.' ) !== false && + ( $filename === '.' || $filename === '..' || + strpos( $filename, './' ) === 0 || + strpos( $filename, '../' ) === 0 || + strpos( $filename, '/./' ) !== false || + strpos( $filename, '/../' ) !== false ) ) + { + return false; + } else { + return true; + } + } + + /**#@+ + * Path disclosure protection functions + */ + function paranoidClean( $param ) { return '[hidden]'; } + function passThrough( $param ) { return $param; } + + /** + * Get a callback function to use for cleaning error message parameters + */ + function getErrorCleanupFunction() { + switch ( $this->pathDisclosureProtection ) { + case 'none': + $callback = array( $this, 'passThrough' ); + break; + default: // 'paranoid' + $callback = array( $this, 'paranoidClean' ); + } + return $callback; + } + /**#@-*/ + + /** + * Create a new fatal error + */ + function newFatal( $message /*, parameters...*/ ) { + $params = func_get_args(); + array_unshift( $params, $this ); + return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params ); + } + + /** + * Create a new good result + */ + function newGood( $value = null ) { + return FileRepoStatus::newGood( $this, $value ); + } + + /** + * Delete files in the deleted directory if they are not referenced in the filearchive table + * STUB + */ + function cleanupDeletedBatch( $storageKeys ) {} +} + diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php new file mode 100644 index 00000000..972b2e46 --- /dev/null +++ b/includes/filerepo/FileRepoStatus.php @@ -0,0 +1,171 @@ +ok = false; + return $result; + } + + static function newGood( $repo = false, $value = null ) { + $result = new self( $repo ); + $result->value = $value; + return $result; + } + + function __construct( $repo = false ) { + if ( $repo ) { + $this->cleanCallback = $repo->getErrorCleanupFunction(); + } + } + + function setResult( $ok, $value = null ) { + $this->ok = $ok; + $this->value = $value; + } + + function isGood() { + return $this->ok && !$this->errors; + } + + function isOK() { + return $this->ok; + } + + function warning( $message /*, parameters... */ ) { + $params = array_slice( func_get_args(), 1 ); + $this->errors[] = array( + 'type' => 'warning', + 'message' => $message, + 'params' => $params ); + } + + /** + * Add an error, do not set fatal flag + * This can be used for non-fatal errors + */ + function error( $message /*, parameters... */ ) { + $params = array_slice( func_get_args(), 1 ); + $this->errors[] = array( + 'type' => 'error', + 'message' => $message, + 'params' => $params ); + } + + /** + * Add an error and set OK to false, indicating that the operation as a whole was fatal + */ + function fatal( $message /*, parameters... */ ) { + $params = array_slice( func_get_args(), 1 ); + $this->errors[] = array( + 'type' => 'error', + 'message' => $message, + 'params' => $params ); + $this->ok = false; + } + + protected function cleanParams( $params ) { + if ( !$this->cleanCallback ) { + return $params; + } + $cleanParams = array(); + foreach ( $params as $i => $param ) { + $cleanParams[$i] = call_user_func( $this->cleanCallback, $param ); + } + return $cleanParams; + } + + protected function getItemXML( $item ) { + $params = $this->cleanParams( $item['params'] ); + $xml = "<{$item['type']}>\n" . + Xml::element( 'message', null, $item['message'] ) . "\n" . + Xml::element( 'text', null, wfMsgReal( $item['message'], $params ) ) ."\n"; + foreach ( $params as $param ) { + $xml .= Xml::element( 'param', null, $param ); + } + $xml .= "type}>\n"; + return $xml; + } + + /** + * Get the error list as XML + */ + function getXML() { + $xml = "\n"; + foreach ( $this->errors as $error ) { + $xml .= $this->getItemXML( $error ); + } + $xml .= "\n"; + return $xml; + } + + /** + * Get the error list as a wikitext formatted list + * @param string $shortContext A short enclosing context message name, to be used + * when there is a single error + * @param string $longContext A long enclosing context message name, for a list + */ + function getWikiText( $shortContext = false, $longContext = false ) { + if ( count( $this->errors ) == 0 ) { + if ( $this->ok ) { + $this->fatal( 'internalerror_info', + __METHOD__." called for a good result, this is incorrect\n" ); + } else { + $this->fatal( 'internalerror_info', + __METHOD__.": Invalid result object: no error text but not OK\n" ); + } + } + if ( count( $this->errors ) == 1 ) { + $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) ); + $s = wfMsgReal( $this->errors[0]['message'], $params ); + if ( $shortContext ) { + $s = wfMsg( $shortContext, $s ); + } elseif ( $longContext ) { + $s = wfMsg( $longContext, "* $s\n" ); + } + } else { + $s = ''; + foreach ( $this->errors as $error ) { + $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ); + $s .= '* ' . wfMsgReal( $error['message'], $params ) . "\n"; + } + if ( $longContext ) { + $s = wfMsg( $longContext, $s ); + } elseif ( $shortContext ) { + $s = wfMsg( $shortContext, "\n* $s\n" ); + } + } + return $s; + } + + /** + * Merge another status object into this one + */ + function merge( $other, $overwriteValue = false ) { + $this->errors = array_merge( $this->errors, $other->errors ); + $this->ok = $this->ok && $other->ok; + if ( $overwriteValue ) { + $this->value = $other->value; + } + $this->successCount += $other->successCount; + $this->failCount += $other->failCount; + } +} diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php new file mode 100644 index 00000000..4d11640a --- /dev/null +++ b/includes/filerepo/ForeignDBFile.php @@ -0,0 +1,42 @@ +repo->hasSharedCache ) { + $hashedName = md5($this->name); + return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix, + 'file', $hashedName ); + } else { + return false; + } + } + + function publish( /*...*/ ) { + $this->readOnlyError(); + } + + function recordUpload( /*...*/ ) { + $this->readOnlyError(); + } + function restore( /*...*/ ) { + $this->readOnlyError(); + } + function delete( /*...*/ ) { + $this->readOnlyError(); + } + + function getDescriptionUrl() { + // Restore remote behaviour + return File::getDescriptionUrl(); + } + + function getDescriptionText() { + // Restore remote behaviour + return File::getDescriptionText(); + } +} + diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php new file mode 100644 index 00000000..13dcd029 --- /dev/null +++ b/includes/filerepo/ForeignDBRepo.php @@ -0,0 +1,57 @@ +dbType = $info['dbType']; + $this->dbServer = $info['dbServer']; + $this->dbUser = $info['dbUser']; + $this->dbPassword = $info['dbPassword']; + $this->dbName = $info['dbName']; + $this->dbFlags = $info['dbFlags']; + $this->tablePrefix = $info['tablePrefix']; + $this->hasSharedCache = $info['hasSharedCache']; + } + + function getMasterDB() { + if ( !isset( $this->dbConn ) ) { + $class = 'Database' . ucfirst( $this->dbType ); + $this->dbConn = new $class( $this->dbServer, $this->dbUser, + $this->dbPassword, $this->dbName, false, $this->dbFlags, + $this->tablePrefix ); + } + return $this->dbConn; + } + + function getSlaveDB() { + return $this->getMasterDB(); + } + + function hasSharedCache() { + return $this->hasSharedCache; + } + + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } + function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } + function deleteBatch( $fileMap ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } +} + + diff --git a/includes/filerepo/ICRepo.php b/includes/filerepo/ICRepo.php new file mode 100644 index 00000000..124fe2b6 --- /dev/null +++ b/includes/filerepo/ICRepo.php @@ -0,0 +1,313 @@ +directory = $info['directory']; + $this->url = $info['url']; + $this->hashLevels = $info['hashLevels']; + if(isset($info['cache'])){ + $this->cache = getcwd().'/images/'.$info['cache']; + } + } +} + +/** + * A file loaded from InstantCommons + */ +class ICFile extends LocalFile{ + static function newFromTitle($title,$repo){ + return new self($title, $repo); + } + + /** + * Returns true if the file comes from the local file repository. + * + * @return bool + */ + function isLocal() { + return true; + } + + function load(){ + if (!$this->dataLoaded ) { + if ( !$this->loadFromCache() ) { + if(!$this->loadFromDB()){ + $this->loadFromIC(); + } + $this->saveToCache(); + } + $this->dataLoaded = true; + } + } + + /** + * Load file metadata from the DB + */ + function loadFromDB() { + wfProfileIn( __METHOD__ ); + + # Unconditionally set loaded=true, we don't want the accessors constantly rechecking + $this->dataLoaded = true; + + $dbr = $this->repo->getSlaveDB(); + + $row = $dbr->selectRow( 'ic_image', $this->getCacheFields( 'img_' ), + array( 'img_name' => $this->getName() ), __METHOD__ ); + if ( $row ) { + if (trim($row->img_media_type)==NULL) { + $this->upgradeRow(); + $this->upgraded = true; + } + $this->loadFromRow( $row ); + //This means that these files are local so the repository locations are local + $this->setUrlPathLocal(); + $this->fileExists = true; + //var_dump($this); exit; + } else { + $this->fileExists = false; + } + + wfProfileOut( __METHOD__ ); + + return $this->fileExists; + } + + /** + * Fix assorted version-related problems with the image row by reloading it from the file + */ + function upgradeRow() { + wfProfileIn( __METHOD__ ); + + $this->loadFromIC(); + + $dbw = $this->repo->getMasterDB(); + list( $major, $minor ) = self::splitMime( $this->mime ); + + wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n"); + + $dbw->update( 'ic_image', + array( + 'img_width' => $this->width, + 'img_height' => $this->height, + 'img_bits' => $this->bits, + 'img_media_type' => $this->type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, + 'img_metadata' => $this->metadata, + ), array( 'img_name' => $this->getName() ), + __METHOD__ + ); + $this->saveToCache(); + wfProfileOut( __METHOD__ ); + } + + function exists(){ + $this->load(); + return $this->fileExists; + + } + + /** + * Fetch the file from the repository. Check local ic_images table first. If not available, check remote server + */ + function loadFromIC(){ + # Unconditionally set loaded=true, we don't want the accessors constantly rechecking + $this->dataLoaded = true; + $icUrl = $this->repo->directory.'&media='.$this->title->mDbkeyform; + if($h = @fopen($icUrl, 'rb')){ + $contents = fread($h, 3000); + $image = $this->api_xml_to_array($contents); + if($image['fileExists']){ + foreach($image as $property=>$value){ + if($property=="url"){$value=$this->repo->url.$value; } + $this->$property = $value; + } + if($this->curl_file_get_contents($this->repo->url.$image['url'], $this->repo->cache.'/'.$image['name'])){ + //Record the image + $this->recordDownload("Downloaded with InstantCommons"); + + //Then cache it + }else{//set fileExists back to false + $this->fileExists = false; + } + } + } + } + + + function setUrlPathLocal(){ + global $wgScriptPath; + $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath)); + $this->repo->url = $path;//.'/'.rawurlencode($this->title->mDbkeyform); + $this->repo->directory = $this->repo->cache;//.'/'.rawurlencode($this->title->mDbkeyform); + + } + + function getThumbPath( $suffix=false ){ + $path = $this->repo->cache; + if ( $suffix !== false ) { + $path .= '/thumb/' . rawurlencode( $suffix ); + } + return $path; + } + function getThumbUrl( $suffix=false ){ + global $wgScriptPath; + $path = $wgScriptPath.'/'.substr($this->repo->cache, strlen($wgScriptPath)); + if ( $suffix !== false ) { + $path .= '/thumb/' . rawurlencode( $suffix ); + } + return $path; + } + + /** + * Convert the InstantCommons Server API XML Response to an associative array + */ + function api_xml_to_array($xml){ + preg_match("//",$xml,$match); + preg_match_all("/(.*?=\".*?\")/",$match[1], $matches); + foreach($matches[1] as $match){ + list($key,$value) = split("=",$match); + $image[trim($key,'<" ')]=trim($value,' "'); + } + return $image; + } + + /** + * Use cURL to read the content of a URL into a string + * ref: http://groups-beta.google.com/group/comp.lang.php/browse_thread/thread/8efbbaced3c45e3c/d63c7891cf8e380b?lnk=raot + * @param string $url - the URL to fetch + * @param resource $fp - filename to write file contents to + * @param boolean $bg - call cURL in the background (don't hang page until complete) + * @param int $timeout - cURL connect timeout + */ + function curl_file_get_contents($url, $fp, $bg=TRUE, $timeout = 1) { + { + # Call curl in the background to download the file + $cmd = 'curl '.wfEscapeShellArg($url).' -o '.$fp.' &'; + wfDebug('Curl download initiated='.$cmd ); + $success = false; + $file_contents = array(); + $file_contents['err'] = wfShellExec($cmd, $file_contents['return']); + if($file_contents['err']==0){//Success + $success = true; + } + } + return $success; + } + + function getMasterDB() { + if ( !isset( $this->dbConn ) ) { + $class = 'Database' . ucfirst( $this->dbType ); + $this->dbConn = new $class( $this->dbServer, $this->dbUser, + $this->dbPassword, $this->dbName, false, $this->dbFlags, + $this->tablePrefix ); + } + return $this->dbConn; + } + + /** + * Record a file upload in the upload log and the image table + */ + private function recordDownload($comment='', $timestamp = false ){ + global $wgUser; + + $dbw = $this->repo->getMasterDB(); + + if ( $timestamp === false ) { + $timestamp = $dbw->timestamp(); + } + list( $major, $minor ) = self::splitMime( $this->mime ); + + # Test to see if the row exists using INSERT IGNORE + # This avoids race conditions by locking the row until the commit, and also + # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition. + $dbw->insert( 'ic_image', + array( + 'img_name' => $this->getName(), + 'img_size'=> $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, + 'img_timestamp' => $timestamp, + 'img_description' => $comment, + 'img_user' => $wgUser->getID(), + 'img_user_text' => $wgUser->getName(), + 'img_metadata' => $this->metadata, + ), + __METHOD__, + 'IGNORE' + ); + + if( $dbw->affectedRows() == 0 ) { + # Collision, this is an update of a file + # Update the current image row + $dbw->update( 'ic_image', + array( /* SET */ + 'img_size' => $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $this->major_mime, + 'img_minor_mime' => $this->minor_mime, + 'img_timestamp' => $timestamp, + 'img_description' => $comment, + 'img_user' => $wgUser->getID(), + 'img_user_text' => $wgUser->getName(), + 'img_metadata' => $this->metadata, + ), array( /* WHERE */ + 'img_name' => $this->getName() + ), __METHOD__ + ); + } else { + # This is a new file + # Update the image count + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + } + + $descTitle = $this->getTitle(); + $article = new Article( $descTitle ); + + # Add the log entry + $log = new LogPage( 'icdownload' ); + $log->addEntry( 'InstantCommons download', $descTitle, $comment ); + + if( $descTitle->exists() ) { + # Create a null revision + $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false ); + $nullRevision->insertOn( $dbw ); + $article->updateRevisionOn( $dbw, $nullRevision ); + + # Invalidate the cache for the description page + $descTitle->invalidateCache(); + $descTitle->purgeSquid(); + } + + + # Commit the transaction now, in case something goes wrong later + # The most important thing is that files don't get lost, especially archives + $dbw->immediateCommit(); + + # Invalidate cache for all pages using this file + $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); + $update->doUpdate(); + + return true; + } + +} + diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php new file mode 100644 index 00000000..1e5fc449 --- /dev/null +++ b/includes/filerepo/LocalFile.php @@ -0,0 +1,1573 @@ +getLocalRepo()->newFile($title); + * + * The convenience functions wfLocalFile() and wfFindFile() should be sufficient + * in most cases. + * + * @addtogroup FileRepo + */ +class LocalFile extends File +{ + /**#@+ + * @private + */ + var $fileExists, # does the file file exist on disk? (loadFromXxx) + $historyLine, # Number of line to return by nextHistoryLine() (constructor) + $historyRes, # result of the query for the file's history (nextHistoryLine) + $width, # \ + $height, # | + $bits, # --- returned by getimagesize (loadFromXxx) + $attr, # / + $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...) + $mime, # MIME type, determined by MimeMagic::guessMimeType + $major_mime, # Major mime type + $minor_mime, # Minor mime type + $size, # Size in bytes (loadFromXxx) + $metadata, # Handler-specific metadata + $timestamp, # Upload timestamp + $sha1, # SHA-1 base 36 content hash + $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) + $upgraded, # Whether the row was upgraded on load + $locked; # True if the image row is locked + + /**#@-*/ + + /** + * Create a LocalFile from a title + * Do not call this except from inside a repo class. + */ + static function newFromTitle( $title, $repo ) { + return new self( $title, $repo ); + } + + /** + * Create a LocalFile from a title + * Do not call this except from inside a repo class. + */ + static function newFromRow( $row, $repo ) { + $title = Title::makeTitle( NS_IMAGE, $row->img_name ); + $file = new self( $title, $repo ); + $file->loadFromRow( $row ); + return $file; + } + + /** + * Constructor. + * Do not call this except from inside a repo class. + */ + function __construct( $title, $repo ) { + if( !is_object( $title ) ) { + throw new MWException( __CLASS__.' constructor given bogus title.' ); + } + parent::__construct( $title, $repo ); + $this->metadata = ''; + $this->historyLine = 0; + $this->historyRes = null; + $this->dataLoaded = false; + } + + /** + * Get the memcached key + */ + function getCacheKey() { + $hashedName = md5($this->getName()); + return wfMemcKey( 'file', $hashedName ); + } + + /** + * Try to load file metadata from memcached. Returns true on success. + */ + function loadFromCache() { + global $wgMemc; + wfProfileIn( __METHOD__ ); + $this->dataLoaded = false; + $key = $this->getCacheKey(); + if ( !$key ) { + return false; + } + $cachedValues = $wgMemc->get( $key ); + + // Check if the key existed and belongs to this version of MediaWiki + if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) { + wfDebug( "Pulling file metadata from cache key $key\n" ); + $this->fileExists = $cachedValues['fileExists']; + if ( $this->fileExists ) { + unset( $cachedValues['version'] ); + unset( $cachedValues['fileExists'] ); + foreach ( $cachedValues as $name => $value ) { + $this->$name = $value; + } + } + } + if ( $this->dataLoaded ) { + wfIncrStats( 'image_cache_hit' ); + } else { + wfIncrStats( 'image_cache_miss' ); + } + + wfProfileOut( __METHOD__ ); + return $this->dataLoaded; + } + + /** + * Save the file metadata to memcached + */ + function saveToCache() { + global $wgMemc; + $this->load(); + $key = $this->getCacheKey(); + if ( !$key ) { + return; + } + $fields = $this->getCacheFields( '' ); + $cache = array( 'version' => MW_FILE_VERSION ); + $cache['fileExists'] = $this->fileExists; + if ( $this->fileExists ) { + foreach ( $fields as $field ) { + $cache[$field] = $this->$field; + } + } + + $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week + } + + /** + * Load metadata from the file itself + */ + function loadFromFile() { + $this->setProps( self::getPropsFromPath( $this->getPath() ) ); + } + + function getCacheFields( $prefix = 'img_' ) { + static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', + 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1' ); + static $results = array(); + if ( $prefix == '' ) { + return $fields; + } + if ( !isset( $results[$prefix] ) ) { + $prefixedFields = array(); + foreach ( $fields as $field ) { + $prefixedFields[] = $prefix . $field; + } + $results[$prefix] = $prefixedFields; + } + return $results[$prefix]; + } + + /** + * Load file metadata from the DB + */ + function loadFromDB() { + # Polymorphic function name to distinguish foreign and local fetches + $fname = get_class( $this ) . '::' . __FUNCTION__; + wfProfileIn( $fname ); + + # Unconditionally set loaded=true, we don't want the accessors constantly rechecking + $this->dataLoaded = true; + + $dbr = $this->repo->getSlaveDB(); + + $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), + array( 'img_name' => $this->getName() ), $fname ); + if ( $row ) { + $this->loadFromRow( $row ); + } else { + $this->fileExists = false; + } + + wfProfileOut( $fname ); + } + + /** + * Decode a row from the database (either object or array) to an array + * with timestamps and MIME types decoded, and the field prefix removed. + */ + function decodeRow( $row, $prefix = 'img_' ) { + $array = (array)$row; + $prefixLength = strlen( $prefix ); + // Sanity check prefix once + if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) { + throw new MWException( __METHOD__. ': incorrect $prefix parameter' ); + } + $decoded = array(); + foreach ( $array as $name => $value ) { + $decoded[substr( $name, $prefixLength )] = $value; + } + $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] ); + if ( empty( $decoded['major_mime'] ) ) { + $decoded['mime'] = "unknown/unknown"; + } else { + if (!$decoded['minor_mime']) { + $decoded['minor_mime'] = "unknown"; + } + $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime']; + } + # Trim zero padding from char/binary field + $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" ); + return $decoded; + } + + /* + * Load file metadata from a DB result row + */ + function loadFromRow( $row, $prefix = 'img_' ) { + $this->dataLoaded = true; + $array = $this->decodeRow( $row, $prefix ); + foreach ( $array as $name => $value ) { + $this->$name = $value; + } + $this->fileExists = true; + // Check for rows from a previous schema, quietly upgrade them + $this->maybeUpgradeRow(); + } + + /** + * Load file metadata from cache or DB, unless already loaded + */ + function load() { + if ( !$this->dataLoaded ) { + if ( !$this->loadFromCache() ) { + $this->loadFromDB(); + $this->saveToCache(); + } + $this->dataLoaded = true; + } + } + + /** + * Upgrade a row if it needs it + */ + function maybeUpgradeRow() { + if ( wfReadOnly() ) { + return; + } + if ( is_null($this->media_type) || + $this->mime == 'image/svg' + ) { + $this->upgradeRow(); + $this->upgraded = true; + } else { + $handler = $this->getHandler(); + if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) { + $this->upgradeRow(); + $this->upgraded = true; + } + } + } + + function getUpgraded() { + return $this->upgraded; + } + + /** + * Fix assorted version-related problems with the image row by reloading it from the file + */ + function upgradeRow() { + wfProfileIn( __METHOD__ ); + + $this->loadFromFile(); + + # Don't destroy file info of missing files + if ( !$this->fileExists ) { + wfDebug( __METHOD__.": file does not exist, aborting\n" ); + return; + } + $dbw = $this->repo->getMasterDB(); + list( $major, $minor ) = self::splitMime( $this->mime ); + + wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n"); + + $dbw->update( 'image', + array( + 'img_width' => $this->width, + 'img_height' => $this->height, + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $major, + 'img_minor_mime' => $minor, + 'img_metadata' => $this->metadata, + 'img_sha1' => $this->sha1, + ), array( 'img_name' => $this->getName() ), + __METHOD__ + ); + $this->saveToCache(); + wfProfileOut( __METHOD__ ); + } + + function setProps( $info ) { + $this->dataLoaded = true; + $fields = $this->getCacheFields( '' ); + $fields[] = 'fileExists'; + foreach ( $fields as $field ) { + if ( isset( $info[$field] ) ) { + $this->$field = $info[$field]; + } + } + // Fix up mime fields + if ( isset( $info['major_mime'] ) ) { + $this->mime = "{$info['major_mime']}/{$info['minor_mime']}"; + } elseif ( isset( $info['mime'] ) ) { + list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime ); + } + } + + /** splitMime inherited */ + /** getName inherited */ + /** getTitle inherited */ + /** getURL inherited */ + /** getViewURL inherited */ + /** getPath inherited */ + + /** + * Return the width of the image + * + * Returns false on error + * @public + */ + function getWidth( $page = 1 ) { + $this->load(); + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['width']; + } else { + return false; + } + } else { + return $this->width; + } + } + + /** + * Return the height of the image + * + * Returns false on error + * @public + */ + function getHeight( $page = 1 ) { + $this->load(); + if ( $this->isMultipage() ) { + $dim = $this->getHandler()->getPageDimensions( $this, $page ); + if ( $dim ) { + return $dim['height']; + } else { + return false; + } + } else { + return $this->height; + } + } + + /** + * Get handler-specific metadata + */ + function getMetadata() { + $this->load(); + return $this->metadata; + } + + /** + * Return the size of the image file, in bytes + * @public + */ + function getSize() { + $this->load(); + return $this->size; + } + + /** + * Returns the mime type of the file. + */ + function getMimeType() { + $this->load(); + return $this->mime; + } + + /** + * Return the type of the media in the file. + * Use the value returned by this function with the MEDIATYPE_xxx constants. + */ + function getMediaType() { + $this->load(); + return $this->media_type; + } + + /** canRender inherited */ + /** mustRender inherited */ + /** allowInlineDisplay inherited */ + /** isSafeFile inherited */ + /** isTrustedFile inherited */ + + /** + * Returns true if the file file exists on disk. + * @return boolean Whether file file exist on disk. + * @public + */ + function exists() { + $this->load(); + return $this->fileExists; + } + + /** getTransformScript inherited */ + /** getUnscaledThumb inherited */ + /** thumbName inherited */ + /** createThumb inherited */ + /** getThumbnail inherited */ + /** transform inherited */ + + /** + * Fix thumbnail files from 1.4 or before, with extreme prejudice + */ + function migrateThumbFile( $thumbName ) { + $thumbDir = $this->getThumbPath(); + $thumbPath = "$thumbDir/$thumbName"; + if ( is_dir( $thumbPath ) ) { + // Directory where file should be + // This happened occasionally due to broken migration code in 1.5 + // Rename to broken-* + for ( $i = 0; $i < 100 ; $i++ ) { + $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName"; + if ( !file_exists( $broken ) ) { + rename( $thumbPath, $broken ); + break; + } + } + // Doesn't exist anymore + clearstatcache(); + } + if ( is_file( $thumbDir ) ) { + // File where directory should be + unlink( $thumbDir ); + // Doesn't exist anymore + clearstatcache(); + } + } + + /** getHandler inherited */ + /** iconThumb inherited */ + /** getLastError inherited */ + + /** + * Get all thumbnail names previously generated for this file + */ + function getThumbnails() { + if ( $this->isHashed() ) { + $this->load(); + $files = array(); + $dir = $this->getThumbPath(); + + if ( is_dir( $dir ) ) { + $handle = opendir( $dir ); + + if ( $handle ) { + while ( false !== ( $file = readdir($handle) ) ) { + if ( $file{0} != '.' ) { + $files[] = $file; + } + } + closedir( $handle ); + } + } + } else { + $files = array(); + } + + return $files; + } + + /** + * Refresh metadata in memcached, but don't touch thumbnails or squid + */ + function purgeMetadataCache() { + $this->loadFromDB(); + $this->saveToCache(); + $this->purgeHistory(); + } + + /** + * Purge the shared history (OldLocalFile) cache + */ + function purgeHistory() { + global $wgMemc; + $hashedName = md5($this->getName()); + $oldKey = wfMemcKey( 'oldfile', $hashedName ); + $wgMemc->delete( $oldKey ); + } + + /** + * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid + */ + function purgeCache() { + // Refresh metadata cache + $this->purgeMetadataCache(); + + // Delete thumbnails + $this->purgeThumbnails(); + + // Purge squid cache for this file + wfPurgeSquidServers( array( $this->getURL() ) ); + } + + /** + * Delete cached transformed files + */ + function purgeThumbnails() { + global $wgUseSquid; + // Delete thumbnails + $files = $this->getThumbnails(); + $dir = $this->getThumbPath(); + $urls = array(); + foreach ( $files as $file ) { + # Check that the base file name is part of the thumb name + # This is a basic sanity check to avoid erasing unrelated directories + if ( strpos( $file, $this->getName() ) !== false ) { + $url = $this->getThumbUrl( $file ); + $urls[] = $url; + @unlink( "$dir/$file" ); + } + } + + // Purge the squid + if ( $wgUseSquid ) { + wfPurgeSquidServers( $urls ); + } + } + + /** purgeDescription inherited */ + /** purgeEverything inherited */ + + /** + * Return the history of this file, line by line. + * starts with current version, then old versions. + * uses $this->historyLine to check which line to return: + * 0 return line for current version + * 1 query for old versions, return first one + * 2, ... return next old version from above query + * + * @public + */ + function nextHistoryLine() { + $dbr = $this->repo->getSlaveDB(); + + if ( $this->historyLine == 0 ) {// called for the first time, return line from cur + $this->historyRes = $dbr->select( 'image', + array( + '*', + "'' AS oi_archive_name" + ), + array( 'img_name' => $this->title->getDBkey() ), + __METHOD__ + ); + if ( 0 == $dbr->numRows( $this->historyRes ) ) { + $dbr->freeResult($this->historyRes); + $this->historyRes = null; + return FALSE; + } + } else if ( $this->historyLine == 1 ) { + $dbr->freeResult($this->historyRes); + $this->historyRes = $dbr->select( 'oldimage', '*', + array( 'oi_name' => $this->title->getDBkey() ), + __METHOD__, + array( 'ORDER BY' => 'oi_timestamp DESC' ) + ); + } + $this->historyLine ++; + + return $dbr->fetchObject( $this->historyRes ); + } + + /** + * Reset the history pointer to the first element of the history + * @public + */ + function resetHistory() { + $this->historyLine = 0; + if (!is_null($this->historyRes)) { + $this->repo->getSlaveDB()->freeResult($this->historyRes); + $this->historyRes = null; + } + } + + /** getFullPath inherited */ + /** getHashPath inherited */ + /** getRel inherited */ + /** getUrlRel inherited */ + /** getArchiveRel inherited */ + /** getThumbRel inherited */ + /** getArchivePath inherited */ + /** getThumbPath inherited */ + /** getArchiveUrl inherited */ + /** getThumbUrl inherited */ + /** getArchiveVirtualUrl inherited */ + /** getThumbVirtualUrl inherited */ + /** isHashed inherited */ + + /** + * Upload a file and record it in the DB + * @param string $srcPath Source path or virtual URL + * @param string $comment Upload description + * @param string $pageText Text to use for the new description page, if a new description page is created + * @param integer $flags Flags for publish() + * @param array $props File properties, if known. This can be used to reduce the + * upload time when uploading virtual URLs for which the file info + * is already known + * @param string $timestamp Timestamp for img_timestamp, or false to use the current time + * + * @return FileRepoStatus object. On success, the value member contains the + * archive name, or an empty string if it was a new file. + */ + function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) { + $this->lock(); + $status = $this->publish( $srcPath, $flags ); + if ( $status->ok ) { + if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) { + $status->fatal( 'filenotfound', $srcPath ); + } + } + $this->unlock(); + return $status; + } + + /** + * Record a file upload in the upload log and the image table + * @deprecated use upload() + */ + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', + $watch = false, $timestamp = false ) + { + $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source ); + if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) { + return false; + } + if ( $watch ) { + global $wgUser; + $wgUser->addWatch( $this->getTitle() ); + } + return true; + + } + + /** + * Record a file upload in the upload log and the image table + */ + function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false ) + { + global $wgUser; + + $dbw = $this->repo->getMasterDB(); + + if ( !$props ) { + $props = $this->repo->getFileProps( $this->getVirtualUrl() ); + } + $this->setProps( $props ); + + // Delete thumbnails and refresh the metadata cache + $this->purgeThumbnails(); + $this->saveToCache(); + wfPurgeSquidServers( array( $this->getURL() ) ); + + // Fail now if the file isn't there + if ( !$this->fileExists ) { + wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" ); + return false; + } + + $reupload = false; + if ( $timestamp === false ) { + $timestamp = $dbw->timestamp(); + } + + # Test to see if the row exists using INSERT IGNORE + # This avoids race conditions by locking the row until the commit, and also + # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition. + $dbw->insert( 'image', + array( + 'img_name' => $this->getName(), + 'img_size'=> $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $this->major_mime, + 'img_minor_mime' => $this->minor_mime, + 'img_timestamp' => $timestamp, + 'img_description' => $comment, + 'img_user' => $wgUser->getID(), + 'img_user_text' => $wgUser->getName(), + 'img_metadata' => $this->metadata, + 'img_sha1' => $this->sha1 + ), + __METHOD__, + 'IGNORE' + ); + + if( $dbw->affectedRows() == 0 ) { + $reupload = true; + + # Collision, this is an update of a file + # Insert previous contents into oldimage + $dbw->insertSelect( 'oldimage', 'image', + array( + 'oi_name' => 'img_name', + 'oi_archive_name' => $dbw->addQuotes( $oldver ), + 'oi_size' => 'img_size', + 'oi_width' => 'img_width', + 'oi_height' => 'img_height', + 'oi_bits' => 'img_bits', + 'oi_timestamp' => 'img_timestamp', + 'oi_description' => 'img_description', + 'oi_user' => 'img_user', + 'oi_user_text' => 'img_user_text', + 'oi_metadata' => 'img_metadata', + 'oi_media_type' => 'img_media_type', + 'oi_major_mime' => 'img_major_mime', + 'oi_minor_mime' => 'img_minor_mime', + 'oi_sha1' => 'img_sha1', + ), array( 'img_name' => $this->getName() ), __METHOD__ + ); + + # Update the current image row + $dbw->update( 'image', + array( /* SET */ + 'img_size' => $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $this->major_mime, + 'img_minor_mime' => $this->minor_mime, + 'img_timestamp' => $timestamp, + 'img_description' => $comment, + 'img_user' => $wgUser->getID(), + 'img_user_text' => $wgUser->getName(), + 'img_metadata' => $this->metadata, + 'img_sha1' => $this->sha1 + ), array( /* WHERE */ + 'img_name' => $this->getName() + ), __METHOD__ + ); + } else { + # This is a new file + # Update the image count + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + } + + $descTitle = $this->getTitle(); + $article = new Article( $descTitle ); + + # Add the log entry + $log = new LogPage( 'upload' ); + $action = $reupload ? 'overwrite' : 'upload'; + $log->addEntry( $action, $descTitle, $comment ); + + if( $descTitle->exists() ) { + # Create a null revision + $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false ); + $nullRevision->insertOn( $dbw ); + $article->updateRevisionOn( $dbw, $nullRevision ); + + # Invalidate the cache for the description page + $descTitle->invalidateCache(); + $descTitle->purgeSquid(); + } else { + // New file; create the description page. + // There's already a log entry, so don't make a second RC entry + $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC ); + } + + # Hooks, hooks, the magic of hooks... + wfRunHooks( 'FileUpload', array( $this ) ); + + # Commit the transaction now, in case something goes wrong later + # The most important thing is that files don't get lost, especially archives + $dbw->immediateCommit(); + + # Invalidate cache for all pages using this file + $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); + $update->doUpdate(); + + return true; + } + + /** + * Move or copy a file to its public location. If a file exists at the + * destination, move it to an archive. Returns the archive name on success + * or an empty string if it was a new file, and a wikitext-formatted + * WikiError object on failure. + * + * The archive name should be passed through to recordUpload for database + * registration. + * + * @param string $sourcePath Local filesystem path to the source image + * @param integer $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move + * rather than copy + * @return FileRepoStatus object. On success, the value member contains the + * archive name, or an empty string if it was a new file. + */ + function publish( $srcPath, $flags = 0 ) { + $this->lock(); + $dstRel = $this->getRel(); + $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName(); + $archiveRel = 'archive/' . $this->getHashPath() . $archiveName; + $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; + $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags ); + if ( $status->value == 'new' ) { + $status->value = ''; + } else { + $status->value = $archiveName; + } + $this->unlock(); + return $status; + } + + /** getLinksTo inherited */ + /** getExifData inherited */ + /** isLocal inherited */ + /** wasDeleted inherited */ + + /** + * Delete all versions of the file. + * + * Moves the files into an archive directory (or deletes them) + * and removes the database rows. + * + * Cache purging is done; logging is caller's responsibility. + * + * @param $reason + * @return FileRepoStatus object. + */ + function delete( $reason ) { + $this->lock(); + $batch = new LocalFileDeleteBatch( $this, $reason ); + $batch->addCurrent(); + + # Get old version relative paths + $dbw = $this->repo->getMasterDB(); + $result = $dbw->select( 'oldimage', + array( 'oi_archive_name' ), + array( 'oi_name' => $this->getName() ) ); + while ( $row = $dbw->fetchObject( $result ) ) { + $batch->addOld( $row->oi_archive_name ); + } + $status = $batch->execute(); + + if ( $status->ok ) { + // Update site_stats + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ ); + $this->purgeEverything(); + } + + $this->unlock(); + return $status; + } + + /** + * Delete an old version of the file. + * + * Moves the file into an archive directory (or deletes it) + * and removes the database row. + * + * Cache purging is done; logging is caller's responsibility. + * + * @param $reason + * @throws MWException or FSException on database or filestore failure + * @return FileRepoStatus object. + */ + function deleteOld( $archiveName, $reason ) { + $this->lock(); + $batch = new LocalFileDeleteBatch( $this, $reason ); + $batch->addOld( $archiveName ); + $status = $batch->execute(); + $this->unlock(); + if ( $status->ok ) { + $this->purgeDescription(); + $this->purgeHistory(); + } + return $status; + } + + /** + * Restore all or specified deleted revisions to the given file. + * Permissions and logging are left to the caller. + * + * May throw database exceptions on error. + * + * @param $versions set of record ids of deleted items to restore, + * or empty to restore all revisions. + * @return FileRepoStatus + */ + function restore( $versions = array(), $unsuppress = false ) { + $batch = new LocalFileRestoreBatch( $this ); + if ( !$versions ) { + $batch->addAll(); + } else { + $batch->addIds( $versions ); + } + $status = $batch->execute(); + if ( !$status->ok ) { + return $status; + } + + $cleanupStatus = $batch->cleanup(); + $cleanupStatus->successCount = 0; + $cleanupStatus->failCount = 0; + $status->merge( $cleanupStatus ); + return $status; + } + + /** isMultipage inherited */ + /** pageCount inherited */ + /** scaleHeight inherited */ + /** getImageSize inherited */ + + /** + * Get the URL of the file description page. + */ + function getDescriptionUrl() { + return $this->title->getLocalUrl(); + } + + /** + * Get the HTML text of the description page + * This is not used by ImagePage for local files, since (among other things) + * it skips the parser cache. + */ + function getDescriptionText() { + global $wgParser; + $revision = Revision::newFromTitle( $this->title ); + if ( !$revision ) return false; + $text = $revision->getText(); + if ( !$text ) return false; + $html = $wgParser->parse( $text, new ParserOptions ); + return $html; + } + + function getTimestamp() { + $this->load(); + return $this->timestamp; + } + + function getSha1() { + $this->load(); + // Initialise now if necessary + if ( $this->sha1 == '' && $this->fileExists ) { + $this->sha1 = File::sha1Base36( $this->getPath() ); + if ( strval( $this->sha1 ) != '' ) { + $dbw = $this->repo->getMasterDB(); + $dbw->update( 'image', + array( 'img_sha1' => $this->sha1 ), + array( 'img_name' => $this->getName() ), + __METHOD__ ); + $this->saveToCache(); + } + } + + return $this->sha1; + } + + /** + * Start a transaction and lock the image for update + * Increments a reference counter if the lock is already held + * @return boolean True if the image exists, false otherwise + */ + function lock() { + $dbw = $this->repo->getMasterDB(); + if ( !$this->locked ) { + $dbw->begin(); + $this->locked++; + } + return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ ); + } + + /** + * Decrement the lock reference count. If the reference count is reduced to zero, commits + * the transaction and thereby releases the image lock. + */ + function unlock() { + if ( $this->locked ) { + --$this->locked; + if ( !$this->locked ) { + $dbw = $this->repo->getMasterDB(); + $dbw->commit(); + } + } + } + + /** + * Roll back the DB transaction and mark the image unlocked + */ + function unlockAndRollback() { + $this->locked = false; + $dbw = $this->repo->getMasterDB(); + $dbw->rollback(); + } +} // LocalFile class + +#------------------------------------------------------------------------------ + +/** + * Backwards compatibility class + */ +class Image extends LocalFile { + function __construct( $title ) { + $repo = RepoGroup::singleton()->getLocalRepo(); + parent::__construct( $title, $repo ); + } + + /** + * Wrapper for wfFindFile(), for backwards-compatibility only + * Do not use in core code. + * @deprecated + */ + static function newFromTitle( $title, $time = false ) { + $img = wfFindFile( $title, $time ); + if ( !$img ) { + $img = wfLocalFile( $title ); + } + return $img; + } + + /** + * Wrapper for wfFindFile(), for backwards-compatibility only. + * Do not use in core code. + * + * @param string $name name of the image, used to create a title object using Title::makeTitleSafe + * @return image object or null if invalid title + * @deprecated + */ + static function newFromName( $name ) { + $title = Title::makeTitleSafe( NS_IMAGE, $name ); + if ( is_object( $title ) ) { + $img = wfFindFile( $title ); + if ( !$img ) { + $img = wfLocalFile( $title ); + } + return $img; + } else { + return NULL; + } + } + + /** + * Return the URL of an image, provided its name. + * + * Backwards-compatibility for extensions. + * Note that fromSharedDirectory will only use the shared path for files + * that actually exist there now, and will return local paths otherwise. + * + * @param string $name Name of the image, without the leading "Image:" + * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath? + * @return string URL of $name image + * @deprecated + */ + static function imageUrl( $name, $fromSharedDirectory = false ) { + $image = null; + if( $fromSharedDirectory ) { + $image = wfFindFile( $name ); + } + if( !$image ) { + $image = wfLocalFile( $name ); + } + return $image->getUrl(); + } +} + +#------------------------------------------------------------------------------ + +/** + * Helper class for file deletion + */ +class LocalFileDeleteBatch { + var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch; + var $status; + + function __construct( File $file, $reason = '' ) { + $this->file = $file; + $this->reason = $reason; + $this->status = $file->repo->newGood(); + } + + function addCurrent() { + $this->srcRels['.'] = $this->file->getRel(); + } + + function addOld( $oldName ) { + $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName ); + $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName ); + } + + function getOldRels() { + if ( !isset( $this->srcRels['.'] ) ) { + $oldRels =& $this->srcRels; + $deleteCurrent = false; + } else { + $oldRels = $this->srcRels; + unset( $oldRels['.'] ); + $deleteCurrent = true; + } + return array( $oldRels, $deleteCurrent ); + } + + /*protected*/ function getHashes() { + $hashes = array(); + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + if ( $deleteCurrent ) { + $hashes['.'] = $this->file->getSha1(); + } + if ( count( $oldRels ) ) { + $dbw = $this->file->repo->getMasterDB(); + $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ), + 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')', + __METHOD__ ); + while ( $row = $dbw->fetchObject( $res ) ) { + if ( rtrim( $row->oi_sha1, "\0" ) === '' ) { + // Get the hash from the file + $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name ); + $props = $this->file->repo->getFileProps( $oldUrl ); + if ( $props['fileExists'] ) { + // Upgrade the oldimage row + $dbw->update( 'oldimage', + array( 'oi_sha1' => $props['sha1'] ), + array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ), + __METHOD__ ); + $hashes[$row->oi_archive_name] = $props['sha1']; + } else { + $hashes[$row->oi_archive_name] = false; + } + } else { + $hashes[$row->oi_archive_name] = $row->oi_sha1; + } + } + } + $missing = array_diff_key( $this->srcRels, $hashes ); + foreach ( $missing as $name => $rel ) { + $this->status->error( 'filedelete-old-unregistered', $name ); + } + foreach ( $hashes as $name => $hash ) { + if ( !$hash ) { + $this->status->error( 'filedelete-missing', $this->srcRels[$name] ); + unset( $hashes[$name] ); + } + } + + return $hashes; + } + + function doDBInserts() { + global $wgUser; + $dbw = $this->file->repo->getMasterDB(); + $encTimestamp = $dbw->addQuotes( $dbw->timestamp() ); + $encUserId = $dbw->addQuotes( $wgUser->getId() ); + $encReason = $dbw->addQuotes( $this->reason ); + $encGroup = $dbw->addQuotes( 'deleted' ); + $ext = $this->file->getExtension(); + $dotExt = $ext === '' ? '' : ".$ext"; + $encExt = $dbw->addQuotes( $dotExt ); + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + + if ( $deleteCurrent ) { + $where = array( 'img_name' => $this->file->getName() ); + $dbw->insertSelect( 'filearchive', 'image', + array( + 'fa_storage_group' => $encGroup, + 'fa_storage_key' => "IF(img_sha1='', '', CONCAT(img_sha1,$encExt))", + + 'fa_deleted_user' => $encUserId, + 'fa_deleted_timestamp' => $encTimestamp, + 'fa_deleted_reason' => $encReason, + 'fa_deleted' => 0, + + 'fa_name' => 'img_name', + 'fa_archive_name' => 'NULL', + 'fa_size' => 'img_size', + 'fa_width' => 'img_width', + 'fa_height' => 'img_height', + 'fa_metadata' => 'img_metadata', + 'fa_bits' => 'img_bits', + 'fa_media_type' => 'img_media_type', + 'fa_major_mime' => 'img_major_mime', + 'fa_minor_mime' => 'img_minor_mime', + 'fa_description' => 'img_description', + 'fa_user' => 'img_user', + 'fa_user_text' => 'img_user_text', + 'fa_timestamp' => 'img_timestamp' + ), $where, __METHOD__ ); + } + + if ( count( $oldRels ) ) { + $where = array( + 'oi_name' => $this->file->getName(), + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ); + + $dbw->insertSelect( 'filearchive', 'oldimage', + array( + 'fa_storage_group' => $encGroup, + 'fa_storage_key' => "IF(oi_sha1='', '', CONCAT(oi_sha1,$encExt))", + + 'fa_deleted_user' => $encUserId, + 'fa_deleted_timestamp' => $encTimestamp, + 'fa_deleted_reason' => $encReason, + 'fa_deleted' => 0, + + 'fa_name' => 'oi_name', + 'fa_archive_name' => 'oi_archive_name', + 'fa_size' => 'oi_size', + 'fa_width' => 'oi_width', + 'fa_height' => 'oi_height', + 'fa_metadata' => 'oi_metadata', + 'fa_bits' => 'oi_bits', + 'fa_media_type' => 'oi_media_type', + 'fa_major_mime' => 'oi_major_mime', + 'fa_minor_mime' => 'oi_minor_mime', + 'fa_description' => 'oi_description', + 'fa_user' => 'oi_user', + 'fa_user_text' => 'oi_user_text', + 'fa_timestamp' => 'oi_timestamp' + ), $where, __METHOD__ ); + } + } + + function doDBDeletes() { + $dbw = $this->file->repo->getMasterDB(); + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + if ( $deleteCurrent ) { + $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ ); + } + if ( count( $oldRels ) ) { + $dbw->delete( 'oldimage', + array( + 'oi_name' => $this->file->getName(), + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' + ), __METHOD__ ); + } + } + + /** + * Run the transaction + */ + function execute() { + global $wgUser, $wgUseSquid; + wfProfileIn( __METHOD__ ); + + $this->file->lock(); + + // Prepare deletion batch + $hashes = $this->getHashes(); + $this->deletionBatch = array(); + $ext = $this->file->getExtension(); + $dotExt = $ext === '' ? '' : ".$ext"; + foreach ( $this->srcRels as $name => $srcRel ) { + // Skip files that have no hash (missing source) + if ( isset( $hashes[$name] ) ) { + $hash = $hashes[$name]; + $key = $hash . $dotExt; + $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; + $this->deletionBatch[$name] = array( $srcRel, $dstRel ); + } + } + + // Lock the filearchive rows so that the files don't get deleted by a cleanup operation + // We acquire this lock by running the inserts now, before the file operations. + // + // This potentially has poor lock contention characteristics -- an alternative + // scheme would be to insert stub filearchive entries with no fa_name and commit + // them in a separate transaction, then run the file ops, then update the fa_name fields. + $this->doDBInserts(); + + // Execute the file deletion batch + $status = $this->file->repo->deleteBatch( $this->deletionBatch ); + if ( !$status->isGood() ) { + $this->status->merge( $status ); + } + + if ( !$this->status->ok ) { + // Critical file deletion error + // Roll back inserts, release lock and abort + // TODO: delete the defunct filearchive rows if we are using a non-transactional DB + $this->file->unlockAndRollback(); + return $this->status; + } + + // Purge squid + if ( $wgUseSquid ) { + $urls = array(); + foreach ( $this->srcRels as $srcRel ) { + $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) ); + $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel; + } + SquidUpdate::purge( $urls ); + } + + // Delete image/oldimage rows + $this->doDBDeletes(); + + // Commit and return + $this->file->unlock(); + wfProfileOut( __METHOD__ ); + return $this->status; + } +} + +#------------------------------------------------------------------------------ + +/** + * Helper class for file undeletion + */ +class LocalFileRestoreBatch { + var $file, $cleanupBatch, $ids, $all, $unsuppress = false; + + function __construct( File $file ) { + $this->file = $file; + $this->cleanupBatch = $this->ids = array(); + $this->ids = array(); + } + + /** + * Add a file by ID + */ + function addId( $fa_id ) { + $this->ids[] = $fa_id; + } + + /** + * Add a whole lot of files by ID + */ + function addIds( $ids ) { + $this->ids = array_merge( $this->ids, $ids ); + } + + /** + * Add all revisions of the file + */ + function addAll() { + $this->all = true; + } + + /** + * Run the transaction, except the cleanup batch. + * The cleanup batch should be run in a separate transaction, because it locks different + * rows and there's no need to keep the image row locked while it's acquiring those locks + * The caller may have its own transaction open. + * So we save the batch and let the caller call cleanup() + */ + function execute() { + global $wgUser, $wgLang; + if ( !$this->all && !$this->ids ) { + // Do nothing + return $this->file->repo->newGood(); + } + + $exists = $this->file->lock(); + $dbw = $this->file->repo->getMasterDB(); + $status = $this->file->repo->newGood(); + + // Fetch all or selected archived revisions for the file, + // sorted from the most recent to the oldest. + $conditions = array( 'fa_name' => $this->file->getName() ); + if( !$this->all ) { + $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')'; + } + + $result = $dbw->select( 'filearchive', '*', + $conditions, + __METHOD__, + array( 'ORDER BY' => 'fa_timestamp DESC' ) ); + + $idsPresent = array(); + $storeBatch = array(); + $insertBatch = array(); + $insertCurrent = false; + $deleteIds = array(); + $first = true; + $archiveNames = array(); + while( $row = $dbw->fetchObject( $result ) ) { + $idsPresent[] = $row->fa_id; + if ( $this->unsuppress ) { + // Currently, fa_deleted flags fall off upon restore, lets be careful about this + } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { + // Skip restoring file revisions that the user cannot restore + continue; + } + if ( $row->fa_name != $this->file->getName() ) { + $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) ); + $status->failCount++; + continue; + } + if ( $row->fa_storage_key == '' ) { + // Revision was missing pre-deletion + $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) ); + $status->failCount++; + continue; + } + + $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key; + $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; + + $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) ); + # Fix leading zero + if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) { + $sha1 = substr( $sha1, 1 ); + } + + if( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown' + || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown' + || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN' + || is_null( $row->fa_metadata ) ) { + // Refresh our metadata + // Required for a new current revision; nice for older ones too. :) + $props = RepoGroup::singleton()->getFileProps( $deletedUrl ); + } else { + $props = array( + 'minor_mime' => $row->fa_minor_mime, + 'major_mime' => $row->fa_major_mime, + 'media_type' => $row->fa_media_type, + 'metadata' => $row->fa_metadata ); + } + + if ( $first && !$exists ) { + // This revision will be published as the new current version + $destRel = $this->file->getRel(); + $insertCurrent = array( + 'img_name' => $row->fa_name, + 'img_size' => $row->fa_size, + 'img_width' => $row->fa_width, + 'img_height' => $row->fa_height, + 'img_metadata' => $props['metadata'], + 'img_bits' => $row->fa_bits, + 'img_media_type' => $props['media_type'], + 'img_major_mime' => $props['major_mime'], + 'img_minor_mime' => $props['minor_mime'], + 'img_description' => $row->fa_description, + 'img_user' => $row->fa_user, + 'img_user_text' => $row->fa_user_text, + 'img_timestamp' => $row->fa_timestamp, + 'img_sha1' => $sha1); + } else { + $archiveName = $row->fa_archive_name; + if( $archiveName == '' ) { + // This was originally a current version; we + // have to devise a new archive name for it. + // Format is ! + $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp ); + do { + $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name; + $timestamp++; + } while ( isset( $archiveNames[$archiveName] ) ); + } + $archiveNames[$archiveName] = true; + $destRel = $this->file->getArchiveRel( $archiveName ); + $insertBatch[] = array( + 'oi_name' => $row->fa_name, + 'oi_archive_name' => $archiveName, + 'oi_size' => $row->fa_size, + 'oi_width' => $row->fa_width, + 'oi_height' => $row->fa_height, + 'oi_bits' => $row->fa_bits, + 'oi_description' => $row->fa_description, + 'oi_user' => $row->fa_user, + 'oi_user_text' => $row->fa_user_text, + 'oi_timestamp' => $row->fa_timestamp, + 'oi_metadata' => $props['metadata'], + 'oi_media_type' => $props['media_type'], + 'oi_major_mime' => $props['major_mime'], + 'oi_minor_mime' => $props['minor_mime'], + 'oi_deleted' => $row->fa_deleted, + 'oi_sha1' => $sha1 ); + } + + $deleteIds[] = $row->fa_id; + $storeBatch[] = array( $deletedUrl, 'public', $destRel ); + $this->cleanupBatch[] = $row->fa_storage_key; + $first = false; + } + unset( $result ); + + // Add a warning to the status object for missing IDs + $missingIds = array_diff( $this->ids, $idsPresent ); + foreach ( $missingIds as $id ) { + $status->error( 'undelete-missing-filearchive', $id ); + } + + // Run the store batch + // Use the OVERWRITE_SAME flag to smooth over a common error + $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); + $status->merge( $storeStatus ); + + if ( !$status->ok ) { + // Store batch returned a critical error -- this usually means nothing was stored + // Stop now and return an error + $this->file->unlock(); + return $status; + } + + // Run the DB updates + // Because we have locked the image row, key conflicts should be rare. + // If they do occur, we can roll back the transaction at this time with + // no data loss, but leaving unregistered files scattered throughout the + // public zone. + // This is not ideal, which is why it's important to lock the image row. + if ( $insertCurrent ) { + $dbw->insert( 'image', $insertCurrent, __METHOD__ ); + } + if ( $insertBatch ) { + $dbw->insert( 'oldimage', $insertBatch, __METHOD__ ); + } + if ( $deleteIds ) { + $dbw->delete( 'filearchive', + array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), + __METHOD__ ); + } + + if( $status->successCount > 0 ) { + if( !$exists ) { + wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" ); + + // Update site_stats + $site_stats = $dbw->tableName( 'site_stats' ); + $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + + $this->file->purgeEverything(); + } else { + wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" ); + $this->file->purgeDescription(); + $this->file->purgeHistory(); + } + } + $this->file->unlock(); + return $status; + } + + /** + * Delete unused files in the deleted zone. + * This should be called from outside the transaction in which execute() was called. + */ + function cleanup() { + if ( !$this->cleanupBatch ) { + return $this->file->repo->newGood(); + } + $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch ); + return $status; + } +} diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php new file mode 100644 index 00000000..72f9e9a6 --- /dev/null +++ b/includes/filerepo/LocalRepo.php @@ -0,0 +1,65 @@ +img_name ) ) { + return LocalFile::newFromRow( $row, $this ); + } elseif ( isset( $row->oi_name ) ) { + return OldLocalFile::newFromRow( $row, $this ); + } else { + throw new MWException( __METHOD__.': invalid row' ); + } + } + + function newFromArchiveName( $title, $archiveName ) { + return OldLocalFile::newFromArchiveName( $title, $this, $archiveName ); + } + + /** + * Delete files in the deleted directory if they are not referenced in the + * filearchive table. This needs to be done in the repo because it needs to + * interleave database locks with file operations, which is potentially a + * remote operation. + * @return FileRepoStatus + */ + function cleanupDeletedBatch( $storageKeys ) { + $root = $this->getZonePath( 'deleted' ); + $dbw = $this->getMasterDB(); + $status = $this->newGood(); + $storageKeys = array_unique($storageKeys); + foreach ( $storageKeys as $key ) { + $hashPath = $this->getDeletedHashPath( $key ); + $path = "$root/$hashPath$key"; + $dbw->begin(); + $inuse = $dbw->selectField( 'filearchive', '1', + array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), + __METHOD__, array( 'FOR UPDATE' ) ); + if ( !$inuse ) { + wfDebug( __METHOD__ . ": deleting $key\n" ); + if ( !@unlink( $path ) ) { + $status->error( 'undelete-cleanup-error', $path ); + $status->failCount++; + } + } else { + wfDebug( __METHOD__ . ": $key still in use\n" ); + $status->successCount++; + } + $dbw->commit(); + } + return $status; + } +} diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php new file mode 100644 index 00000000..850a8d8a --- /dev/null +++ b/includes/filerepo/OldLocalFile.php @@ -0,0 +1,232 @@ +oi_name ); + $file = new self( $title, $repo, null, $row->oi_archive_name ); + $file->loadFromRow( $row, 'oi_' ); + return $file; + } + + /** + * @param Title $title + * @param FileRepo $repo + * @param string $time Timestamp or null to load by archive name + * @param string $archiveName Archive name or null to load by timestamp + */ + function __construct( $title, $repo, $time, $archiveName ) { + parent::__construct( $title, $repo ); + $this->requestedTime = $time; + $this->archive_name = $archiveName; + if ( is_null( $time ) && is_null( $archiveName ) ) { + throw new MWException( __METHOD__.': must specify at least one of $time or $archiveName' ); + } + } + + function getCacheKey() { + $hashedName = md5($this->getName()); + return wfMemcKey( 'oldfile', $hashedName ); + } + + function getArchiveName() { + if ( !isset( $this->archive_name ) ) { + $this->load(); + } + return $this->archive_name; + } + + function isOld() { + return true; + } + + /** + * Try to load file metadata from memcached. Returns true on success. + */ + function loadFromCache() { + global $wgMemc; + wfProfileIn( __METHOD__ ); + $this->dataLoaded = false; + $key = $this->getCacheKey(); + if ( !$key ) { + return false; + } + $oldImages = $wgMemc->get( $key ); + + if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) { + unset( $oldImages['version'] ); + $more = isset( $oldImages['more'] ); + unset( $oldImages['more'] ); + $found = false; + if ( is_null( $this->requestedTime ) ) { + foreach ( $oldImages as $timestamp => $info ) { + if ( $info['archive_name'] == $this->archive_name ) { + $found = true; + break; + } + } + } else { + krsort( $oldImages ); + foreach ( $oldImages as $timestamp => $info ) { + if ( $timestamp <= $this->requestedTime ) { + $found = true; + break; + } + } + } + if ( $found ) { + wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" ); + $this->dataLoaded = true; + $this->fileExists = true; + foreach ( $info as $name => $value ) { + $this->$name = $value; + } + } elseif ( $more ) { + wfDebug( "Cache key was truncated, oldimage row might be found in the database\n" ); + } else { + wfDebug( "Image did not exist at the specified time.\n" ); + $this->fileExists = false; + $this->dataLoaded = true; + } + } + + if ( $this->dataLoaded ) { + wfIncrStats( 'image_cache_hit' ); + } else { + wfIncrStats( 'image_cache_miss' ); + } + + wfProfileOut( __METHOD__ ); + return $this->dataLoaded; + } + + function saveToCache() { + // If a timestamp was specified, cache the entire history of the image (up to MAX_CACHE_ROWS). + if ( is_null( $this->requestedTime ) ) { + return; + } + // This is expensive, so we only do it if $wgMemc is real + global $wgMemc; + if ( $wgMemc instanceof FakeMemcachedClient ) { + return; + } + $key = $this->getCacheKey(); + if ( !$key ) { + return; + } + wfProfileIn( __METHOD__ ); + + $dbr = $this->repo->getSlaveDB(); + $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ), + array( 'oi_name' => $this->getName() ), __METHOD__, + array( + 'LIMIT' => self::MAX_CACHE_ROWS + 1, + 'ORDER BY' => 'oi_timestamp DESC', + )); + $cache = array( 'version' => self::CACHE_VERSION ); + $numRows = $dbr->numRows( $res ); + if ( $numRows > self::MAX_CACHE_ROWS ) { + $cache['more'] = true; + $numRows--; + } + for ( $i = 0; $i < $numRows; $i++ ) { + $row = $dbr->fetchObject( $res ); + $decoded = $this->decodeRow( $row, 'oi_' ); + $cache[$row->oi_timestamp] = $decoded; + } + $dbr->freeResult( $res ); + $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ ); + wfProfileOut( __METHOD__ ); + } + + function loadFromDB() { + wfProfileIn( __METHOD__ ); + $this->dataLoaded = true; + $dbr = $this->repo->getSlaveDB(); + $conds = array( 'oi_name' => $this->getName() ); + if ( is_null( $this->requestedTime ) ) { + $conds['oi_archive_name'] = $this->archive_name; + } else { + $conds[] = 'oi_timestamp <= ' . $dbr->addQuotes( $this->requestedTime ); + } + $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), + $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) ); + if ( $row ) { + $this->loadFromRow( $row, 'oi_' ); + } else { + $this->fileExists = false; + } + wfProfileOut( __METHOD__ ); + } + + function getCacheFields( $prefix = 'img_' ) { + $fields = parent::getCacheFields( $prefix ); + $fields[] = $prefix . 'archive_name'; + + // XXX: Temporary hack before schema update + //$fields = array_diff( $fields, array( + // 'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) ); + return $fields; + } + + function getRel() { + return 'archive/' . $this->getHashPath() . $this->getArchiveName(); + } + + function getUrlRel() { + return 'archive/' . $this->getHashPath() . urlencode( $this->getArchiveName() ); + } + + function upgradeRow() { + wfProfileIn( __METHOD__ ); + $this->loadFromFile(); + + # Don't destroy file info of missing files + if ( !$this->fileExists ) { + wfDebug( __METHOD__.": file does not exist, aborting\n" ); + wfProfileOut( __METHOD__ ); + return; + } + + $dbw = $this->repo->getMasterDB(); + list( $major, $minor ) = self::splitMime( $this->mime ); + + wfDebug(__METHOD__.': upgrading '.$this->archive_name." to the current schema\n"); + $dbw->update( 'oldimage', + array( + 'oi_width' => $this->width, + 'oi_height' => $this->height, + 'oi_bits' => $this->bits, + 'oi_media_type' => $this->media_type, + 'oi_major_mime' => $major, + 'oi_minor_mime' => $minor, + 'oi_metadata' => $this->metadata, + 'oi_sha1' => $this->sha1, + ), array( + 'oi_name' => $this->getName(), + 'oi_archive_name' => $this->archive_name ), + __METHOD__ + ); + wfProfileOut( __METHOD__ ); + } +} + + + diff --git a/includes/filerepo/README b/includes/filerepo/README new file mode 100644 index 00000000..03cb8b3b --- /dev/null +++ b/includes/filerepo/README @@ -0,0 +1,41 @@ +Some quick notes on the file/repository architecture. + +Functionality is, as always, driven by data model. + +* The repository object stores configuration information about a file storage + method. + +* The file object is a process-local cache of information about a particular + file. + +Thus the file object is the primary public entry point for obtaining information +about files, since access via the file object can be cached, whereas access via +the repository should not be cached. + +Functions which can act on any file specified in their parameters typically find +their place either in the repository object, where reference to +repository-specific configuration is needed, or in static members of File or +FileRepo, where no such configuration is needed. + +File objects are generated by a factory function from the repository. The +repository thus has full control over the behaviour of its subsidiary file +class, since it can subclass the file class and override functionality at its +whim. Thus there is no need for the File subclass to query its parent repository +for information about repository-class-dependent behaviour -- the file subclass +is generally fully aware of the static preferences of its repository. Limited +exceptions can be made to this rule to permit sharing of functions, or perhaps +even entire classes, between repositories. + +These rules alone still do lead to some ambiguity -- it may not be clear whether +to implement some functionality in a repository function with a filename +parameter, or in the file object itself. + +So we introduce the following rule: the file subclass is smarter than the +repository subclass. The repository should in general provide a minimal API +needed to access the storage backend efficiently. + +In particular, note that I have not implemented any database access in +LocalRepo.php. LocalRepo provides only file access, and LocalFile provides +database access and higher-level functions such as cache management. + +Tim Starling, June 2007 diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php new file mode 100644 index 00000000..23d222af --- /dev/null +++ b/includes/filerepo/RepoGroup.php @@ -0,0 +1,150 @@ +localInfo = $localInfo; + $this->foreignInfo = $foreignInfo; + } + + /** + * Search repositories for an image. + * You can also use wfGetFile() to do this. + * @param mixed $title Title object or string + * @param mixed $time The 14-char timestamp before which the file should + * have been uploaded, or false for the current version + * @return File object or false if it is not found + */ + function findFile( $title, $time = false ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $image = $this->localRepo->findFile( $title, $time ); + if ( $image ) { + return $image; + } + foreach ( $this->foreignRepos as $repo ) { + $image = $repo->findFile( $title, $time ); + if ( $image ) { + return $image; + } + } + return false; + } + + /** + * Get the repo instance with a given key. + */ + function getRepo( $index ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + if ( $index == 'local' ) { + return $this->localRepo; + } elseif ( isset( $this->foreignRepos[$index] ) ) { + return $this->foreignRepos[$index]; + } else { + return false; + } + } + + /** + * Get the local repository, i.e. the one corresponding to the local image + * table. Files are typically uploaded to the local repository. + */ + function getLocalRepo() { + return $this->getRepo( 'local' ); + } + + /** + * Initialise the $repos array + */ + function initialiseRepos() { + if ( $this->reposInitialised ) { + return; + } + $this->reposInitialised = true; + + $this->localRepo = $this->newRepo( $this->localInfo ); + $this->foreignRepos = array(); + foreach ( $this->foreignInfo as $key => $info ) { + $this->foreignRepos[$key] = $this->newRepo( $info ); + } + } + + /** + * Create a repo class based on an info structure + */ + protected function newRepo( $info ) { + $class = $info['class']; + return new $class( $info ); + } + + /** + * Split a virtual URL into repo, zone and rel parts + * @return an array containing repo, zone and rel + */ + function splitVirtualUrl( $url ) { + if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { + throw new MWException( __METHOD__.': unknown protoocl' ); + } + + $bits = explode( '/', substr( $url, 9 ), 3 ); + if ( count( $bits ) != 3 ) { + throw new MWException( __METHOD__.": invalid mwrepo URL: $url" ); + } + return $bits; + } + + function getFileProps( $fileName ) { + if ( FileRepo::isVirtualUrl( $fileName ) ) { + list( $repoName, /* $zone */, /* $rel */ ) = $this->splitVirtualUrl( $fileName ); + if ( $repoName === '' ) { + $repoName = 'local'; + } + $repo = $this->getRepo( $repoName ); + return $repo->getFileProps( $fileName ); + } else { + return File::getPropsFromPath( $fileName ); + } + } +} + + diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php new file mode 100644 index 00000000..419c61f6 --- /dev/null +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -0,0 +1,109 @@ +title = $title; + $this->name = $repo->getNameFromTitle( $title ); + } else { + $this->name = basename( $path ); + $this->title = Title::makeTitleSafe( NS_IMAGE, $this->name ); + } + $this->repo = $repo; + if ( $path ) { + $this->path = $path; + } else { + $this->path = $repo->getRootDirectory() . '/' . $repo->getHashPath( $this->name ) . $this->name; + } + if ( $mime ) { + $this->mime = $mime; + } + $this->dims = array(); + } + + function getPageDimensions( $page = 1 ) { + if ( !isset( $this->dims[$page] ) ) { + if ( !$this->getHandler() ) { + return false; + } + $this->dims[$page] = $this->handler->getPageDimensions( $this, $page ); + } + return $this->dims[$page]; + } + + function getWidth( $page = 1 ) { + $dim = $this->getPageDimensions( $page ); + return $dim['width']; + } + + function getHeight( $page = 1 ) { + $dim = $this->getPageDimensions( $page ); + return $dim['height']; + } + + function getMimeType() { + if ( !isset( $this->mime ) ) { + $magic = MimeMagic::singleton(); + $this->mime = $magic->guessMimeType( $this->path ); + } + return $this->mime; + } + + function getImageSize( $filename ) { + if ( !$this->getHandler() ) { + return false; + } + return $this->handler->getImageSize( $this, $this->getPath() ); + } + + function getMetadata() { + if ( !isset( $this->metadata ) ) { + if ( !$this->getHandler() ) { + $this->metadata = false; + } else { + $this->metadata = $this->handler->getMetadata( $this, $this->getPath() ); + } + } + return $this->metadata; + } + + function getURL() { + if ( $this->repo ) { + return $this->repo->getZoneUrl( 'public' ) . '/' . $this->repo->getHashPath( $this->name ) . urlencode( $this->name ); + } else { + return false; + } + } + + function getSize() { + if ( file_exists( $this->path ) ) { + return filesize( $this->path ); + } else { + return false; + } + } +} + diff --git a/includes/media/BMP.php b/includes/media/BMP.php index 9917856a..2f451b0a 100644 --- a/includes/media/BMP.php +++ b/includes/media/BMP.php @@ -1,8 +1,8 @@ +} \ No newline at end of file diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php index 3f3aabbf..ca82aab0 100644 --- a/includes/media/Bitmap.php +++ b/includes/media/Bitmap.php @@ -51,17 +51,20 @@ class BitmapHandler extends ImageHandler { $srcWidth = $image->getWidth(); $srcHeight = $image->getHeight(); $mimeType = $image->getMimeType(); - $srcPath = $image->getImagePath(); + $srcPath = $image->getPath(); $retval = 0; wfDebug( __METHOD__.": creating {$physicalWidth}x{$physicalHeight} thumbnail at $dstPath\n" ); if ( $physicalWidth == $srcWidth && $physicalHeight == $srcHeight ) { # normaliseParams (or the user) wants us to return the unscaled image wfDebug( __METHOD__.": returning unscaled image\n" ); - return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath ); + return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); } - if ( $wgUseImageMagick ) { + if ( !$dstPath ) { + // No output path available, client side scaling only + $scaler = 'client'; + } elseif ( $wgUseImageMagick ) { $scaler = 'im'; } elseif ( $wgCustomConvertCommand ) { $scaler = 'custom'; @@ -74,11 +77,11 @@ class BitmapHandler extends ImageHandler { if ( $scaler == 'client' ) { # Client-side image scaling, use the source URL # Using the destination URL in a TRANSFORM_LATER request would be incorrect - return new ThumbnailImage( $image->getURL(), $clientWidth, $clientHeight, $srcPath ); + return new ThumbnailImage( $image, $image->getURL(), $clientWidth, $clientHeight, $srcPath ); } if ( $flags & self::TRANSFORM_LATER ) { - return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); } if ( !wfMkdirParents( dirname( $dstPath ) ) ) { @@ -164,9 +167,27 @@ class BitmapHandler extends ImageHandler { $src_image = call_user_func( $loader, $srcPath ); $dst_image = imagecreatetruecolor( $physicalWidth, $physicalHeight ); - imagecopyresampled( $dst_image, $src_image, - 0,0,0,0, - $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); + + // Initialise the destination image to transparent instead of + // the default solid black, to support PNG and GIF transparency nicely + $background = imagecolorallocate( $dst_image, 0, 0, 0 ); + imagecolortransparent( $dst_image, $background ); + imagealphablending( $dst_image, false ); + + if( $colorStyle == 'palette' ) { + // Don't resample for paletted GIF images. + // It may just uglify them, and completely breaks transparency. + imagecopyresized( $dst_image, $src_image, + 0,0,0,0, + $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); + } else { + imagecopyresampled( $dst_image, $src_image, + 0,0,0,0, + $physicalWidth, $physicalHeight, imagesx( $src_image ), imagesy( $src_image ) ); + } + + imagesavealpha( $dst_image, true ); + call_user_func( $saveType, $dst_image, $dstPath ); imagedestroy( $dst_image ); imagedestroy( $src_image ); @@ -180,7 +201,7 @@ class BitmapHandler extends ImageHandler { wfHostname(), $retval, trim($err), $cmd ) ); return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); } else { - return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); } } @@ -231,6 +252,56 @@ class BitmapHandler extends ImageHandler { return true; } + /** + * Get a list of EXIF metadata items which should be displayed when + * the metadata table is collapsed. + * + * @return array of strings + * @access private + */ + function visibleMetadataFields() { + $fields = array(); + $lines = explode( "\n", wfMsgForContent( 'metadata-fields' ) ); + foreach( $lines as $line ) { + $matches = array(); + if( preg_match( '/^\\*\s*(.*?)\s*$/', $line, $matches ) ) { + $fields[] = $matches[1]; + } + } + $fields = array_map( 'strtolower', $fields ); + return $fields; + } + + function formatMetadata( $image ) { + $result = array( + 'visible' => array(), + 'collapsed' => array() + ); + $metadata = $image->getMetadata(); + if ( !$metadata ) { + return false; + } + $exif = unserialize( $metadata ); + if ( !$exif ) { + return false; + } + unset( $exif['MEDIAWIKI_EXIF_VERSION'] ); + $format = new FormatExif( $exif ); + + $formatted = $format->getFormattedData(); + // Sort fields into visible and collapsed + $visibleFields = $this->visibleMetadataFields(); + foreach ( $formatted as $name => $value ) { + $tag = strtolower( $name ); + self::addMeta( $result, + in_array( $tag, $visibleFields ) ? 'visible' : 'collapsed', + 'exif', + $tag, + $value + ); + } + return $result; + } } -?> + diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php index 3c053a0c..20e59d18 100644 --- a/includes/media/DjVu.php +++ b/includes/media/DjVu.php @@ -17,6 +17,13 @@ class DjVuHandler extends ImageHandler { function mustRender() { return true; } function isMultiPage() { return true; } + function getParamMap() { + return array( + 'img_width' => 'width', + 'img_page' => 'page', + ); + } + function validateParam( $name, $value ) { if ( in_array( $name, array( 'width', 'height', 'page' ) ) ) { if ( $value <= 0 ) { @@ -69,15 +76,14 @@ class DjVuHandler extends ImageHandler { } $width = $params['width']; $height = $params['height']; - $srcPath = $image->getImagePath(); + $srcPath = $image->getPath(); $page = $params['page']; - $pageCount = $this->pageCount( $image ); if ( $page > $this->pageCount( $image ) ) { return new MediaTransformError( 'thumbnail_error', $width, $height, wfMsg( 'djvu_page_error' ) ); } if ( $flags & self::TRANSFORM_LATER ) { - return new ThumbnailImage( $dstUrl, $width, $height, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page ); } if ( !wfMkdirParents( dirname( $dstPath ) ) ) { @@ -104,7 +110,7 @@ class DjVuHandler extends ImageHandler { wfHostname(), $retval, trim($err), $cmd ) ); return new MediaTransformError( 'thumbnail_error', $width, $height, $err ); } else { - return new ThumbnailImage( $dstUrl, $width, $height, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $width, $height, $dstPath, $page ); } } @@ -203,4 +209,4 @@ class DjVuHandler extends ImageHandler { } } -?> + diff --git a/includes/media/Generic.php b/includes/media/Generic.php index 5254e0ea..c7ab7d81 100644 --- a/includes/media/Generic.php +++ b/includes/media/Generic.php @@ -36,6 +36,12 @@ abstract class MediaHandler { return self::$handlers[$class]; } + /** + * Get an associative array mapping magic word IDs to parameter names. + * Will be used by the parser to identify parameters. + */ + abstract function getParamMap(); + /* * Validate a thumbnail parameter at parse time. * Return true to accept the parameter, and false to reject it. @@ -126,20 +132,20 @@ abstract class MediaHandler { /** * True if the handled types can be transformed */ - function canRender() { return true; } + function canRender( $file ) { return true; } /** * True if handled types cannot be displayed directly in a browser * but can be rendered */ - function mustRender() { return false; } + function mustRender( $file ) { return false; } /** * True if the type has multi-page capabilities */ - function isMultiPage() { return false; } + function isMultiPage( $file ) { return false; } /** * Page count for a multi-page document, false if unsupported or unknown */ - function pageCount() { return false; } + function pageCount( $file ) { return false; } /** * False if the handler is disabled for all files */ @@ -152,12 +158,102 @@ abstract class MediaHandler { * Returns false if unknown or if the document is not multi-page. */ function getPageDimensions( $image, $page ) { - $gis = $this->getImageSize( $image, $image->getImagePath() ); + $gis = $this->getImageSize( $image, $image->getPath() ); return array( 'width' => $gis[0], 'height' => $gis[1] ); } + + /** + * Get an array structure that looks like this: + * + * array( + * 'visible' => array( + * 'Human-readable name' => 'Human readable value', + * ... + * ), + * 'collapsed' => array( + * 'Human-readable name' => 'Human readable value', + * ... + * ) + * ) + * The UI will format this into a table where the visible fields are always + * visible, and the collapsed fields are optionally visible. + * + * The function should return false if there is no metadata to display. + */ + + /** + * FIXME: I don't really like this interface, it's not very flexible + * I think the media handler should generate HTML instead. It can do + * all the formatting according to some standard. That makes it possible + * to do things like visual indication of grouped and chained streams + * in ogg container files. + */ + function formatMetadata( $image, $metadata ) { + return false; + } + + /** + * @fixme document this! + * 'value' thingy goes into a wikitext table; it used to be escaped but + * that was incompatible with previous practice of customized display + * with wikitext formatting via messages such as 'exif-model-value'. + * So the escaping is taken back out, but generally this seems a confusing + * interface. + */ + protected static function addMeta( &$array, $visibility, $type, $id, $value, $param = false ) { + $array[$visibility][] = array( + 'id' => "$type-$id", + 'name' => wfMsg( "$type-$id", $param ), + 'value' => $value + ); + } + + function getShortDesc( $file ) { + global $wgLang; + $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $file->getSize() ) ) . ')'; + return "$nbytes"; + } + + function getLongDesc( $file ) { + global $wgUser; + $sk = $wgUser->getSkin(); + return wfMsg( 'file-info', $sk->formatSize( $file->getSize() ), $file->getMimeType() ); + } + + function getDimensionsString() { + return ''; + } + + /** + * Modify the parser object post-transform + */ + function parserTransformHook( $parser, $file ) {} + + /** + * Check for zero-sized thumbnails. These can be generated when + * no disk space is available or some other error occurs + * + * @param $dstPath The location of the suspect file + * @param $retval Return value of some shell process, file will be deleted if this is non-zero + * @return true if removed, false otherwise + */ + function removeBadFile( $dstPath, $retval = 0 ) { + if( file_exists( $dstPath ) ) { + $thumbstat = stat( $dstPath ); + if( $thumbstat['size'] == 0 || $retval != 0 ) { + wfDebugLog( 'thumbnail', + sprintf( 'Removing bad %d-byte thumbnail "%s"', + $thumbstat['size'], $dstPath ) ); + unlink( $dstPath ); + return true; + } + } + return false; + } } /** @@ -166,6 +262,18 @@ abstract class MediaHandler { * @addtogroup Media */ abstract class ImageHandler extends MediaHandler { + function canRender( $file ) { + if ( $file->getWidth() && $file->getHeight() ) { + return true; + } else { + return false; + } + } + + function getParamMap() { + return array( 'img_width' => 'width' ); + } + function validateParam( $name, $value ) { if ( in_array( $name, array( 'width', 'height' ) ) ) { if ( $value <= 0 ) { @@ -181,8 +289,10 @@ abstract class ImageHandler extends MediaHandler { function makeParamString( $params ) { if ( isset( $params['physicalWidth'] ) ) { $width = $params['physicalWidth']; - } else { + } elseif ( isset( $params['width'] ) ) { $width = $params['width']; + } else { + throw new MWException( 'No width specified to '.__METHOD__ ); } # Removed for ProofreadPage #$width = intval( $width ); @@ -218,7 +328,7 @@ abstract class ImageHandler extends MediaHandler { $params['width'] = wfFitBoxWidth( $srcWidth, $srcHeight, $params['height'] ); } } - $params['height'] = Image::scaleHeight( $srcWidth, $srcHeight, $params['width'] ); + $params['height'] = File::scaleHeight( $srcWidth, $srcHeight, $params['width'] ); if ( !$this->validateThumbParams( $params['width'], $params['height'], $srcWidth, $srcHeight, $mimeType ) ) { return false; } @@ -252,7 +362,7 @@ abstract class ImageHandler extends MediaHandler { return false; } - $height = Image::scaleHeight( $srcWidth, $srcHeight, $width ); + $height = File::scaleHeight( $srcWidth, $srcHeight, $width ); return true; } @@ -261,30 +371,8 @@ abstract class ImageHandler extends MediaHandler { return false; } $url = $script . '&' . wfArrayToCGI( $this->getScriptParams( $params ) ); - return new ThumbnailImage( $url, $params['width'], $params['height'] ); - } - - /** - * Check for zero-sized thumbnails. These can be generated when - * no disk space is available or some other error occurs - * - * @param $dstPath The location of the suspect file - * @param $retval Return value of some shell process, file will be deleted if this is non-zero - * @return true if removed, false otherwise - */ - function removeBadFile( $dstPath, $retval = 0 ) { - $removed = false; - if( file_exists( $dstPath ) ) { - $thumbstat = stat( $dstPath ); - if( $thumbstat['size'] == 0 || $retval != 0 ) { - wfDebugLog( 'thumbnail', - sprintf( 'Removing bad %d-byte thumbnail "%s"', - $thumbstat['size'], $dstPath ) ); - unlink( $dstPath ); - return true; - } - } - return false; + $page = isset( $params['page'] ) ? $params['page'] : false; + return new ThumbnailImage( $image, $url, $params['width'], $params['height'], $page ); } function getImageSize( $image, $path ) { @@ -293,6 +381,31 @@ abstract class ImageHandler extends MediaHandler { wfRestoreWarnings(); return $gis; } + + function getShortDesc( $file ) { + global $wgLang; + $nbytes = '(' . wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), + $wgLang->formatNum( $file->getSize() ) ) . ')'; + $widthheight = wfMsgHtml( 'widthheight', $file->getWidth(), $file->getHeight() ); + + return "$widthheight ($nbytes)"; + } + + function getLongDesc( $file ) { + global $wgLang; + return wfMsgHtml('file-info-size', $file->getWidth(), $file->getHeight(), + $wgLang->formatSize( $file->getSize() ), $file->getMimeType() ); + } + + function getDimensionsString( $file ) { + $pages = $file->pageCount(); + if ( $pages > 1 ) { + return wfMsg( 'widthheightpage', $file->getWidth(), $file->getHeight(), $pages ); + } else { + return wfMsg( 'widthheight', $file->getWidth(), $file->getHeight() ); + } + } } -?> + + diff --git a/includes/media/SVG.php b/includes/media/SVG.php index 5307e269..75d0ad3d 100644 --- a/includes/media/SVG.php +++ b/includes/media/SVG.php @@ -14,7 +14,7 @@ class SvgHandler extends ImageHandler { } } - function mustRender() { + function mustRender( $file ) { return true; } @@ -31,7 +31,7 @@ class SvgHandler extends ImageHandler { $srcWidth = $image->getWidth( $params['page'] ); $srcHeight = $image->getHeight( $params['page'] ); $params['physicalWidth'] = $wgSVGMaxSize; - $params['physicalHeight'] = Image::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize ); + $params['physicalHeight'] = File::scaleHeight( $srcWidth, $srcHeight, $wgSVGMaxSize ); } return true; } @@ -46,12 +46,10 @@ class SvgHandler extends ImageHandler { $clientHeight = $params['height']; $physicalWidth = $params['physicalWidth']; $physicalHeight = $params['physicalHeight']; - $srcWidth = $image->getWidth(); - $srcHeight = $image->getHeight(); - $srcPath = $image->getImagePath(); + $srcPath = $image->getPath(); if ( $flags & self::TRANSFORM_LATER ) { - return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); } if ( !wfMkdirParents( dirname( $dstPath ) ) ) { @@ -82,7 +80,7 @@ class SvgHandler extends ImageHandler { wfHostname(), $retval, trim($err), $cmd ) ); return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err ); } else { - return new ThumbnailImage( $dstUrl, $clientWidth, $clientHeight, $dstPath ); + return new ThumbnailImage( $image, $dstUrl, $clientWidth, $clientHeight, $dstPath ); } } @@ -93,5 +91,12 @@ class SvgHandler extends ImageHandler { function getThumbType( $ext, $mime ) { return array( 'png', 'image/png' ); } + + function getLongDesc( $file ) { + global $wgLang; + return wfMsg( 'svg-long-desc', $file->getWidth(), $file->getHeight(), + $wgLang->formatSize( $file->getSize() ) ); + } } -?> + + diff --git a/includes/memcached-client.php b/includes/memcached-client.php index 1f4bac00..2eddb908 100644 --- a/includes/memcached-client.php +++ b/includes/memcached-client.php @@ -152,7 +152,7 @@ class memcached /** * At how many bytes should we compress? * - * @var interger + * @var integer * @access private */ var $_compress_threshold; @@ -192,7 +192,7 @@ class memcached /** * Total # of bit buckets we have * - * @var interger + * @var integer * @access private */ var $_bucketcount; @@ -200,7 +200,7 @@ class memcached /** * # of total servers we have * - * @var interger + * @var integer * @access private */ var $_active; @@ -272,9 +272,9 @@ class memcached * Adds a key/value to the memcache server if one isn't already set with * that key * - * @param string $key Key to set with data - * @param mixed $val Value to store - * @param interger $exp (optional) Time to expire data at + * @param string $key Key to set with data + * @param mixed $val Value to store + * @param integer $exp (optional) Time to expire data at * * @return boolean * @access public @@ -291,7 +291,7 @@ class memcached * Decriment a value stored on the memcache server * * @param string $key Key to decriment - * @param interger $amt (optional) Amount to decriment + * @param integer $amt (optional) Amount to decriment * * @return mixed FALSE on failure, value on success * @access public @@ -308,7 +308,7 @@ class memcached * Deletes a key from the server, optionally after $time * * @param string $key Key to delete - * @param interger $time (optional) How long to wait before deleting + * @param integer $time (optional) How long to wait before deleting * * @return boolean TRUE on success, FALSE on failure * @access public @@ -506,9 +506,9 @@ class memcached * Increments $key (optionally) by $amt * * @param string $key Key to increment - * @param interger $amt (optional) amount to increment + * @param integer $amt (optional) amount to increment * - * @return interger New key value? + * @return integer New key value? * @access public */ function incr ($key, $amt=1) @@ -524,7 +524,7 @@ class memcached * * @param string $key Key to set value as * @param mixed $value Value to store - * @param interger $exp (optional) Experiation time + * @param integer $exp (optional) Experiation time * * @return boolean * @access public @@ -582,7 +582,7 @@ class memcached * * @param string $key Key to set value as * @param mixed $value Value to set - * @param interger $exp (optional) Experiation time + * @param integer $exp (optional) Experiation time * * @return boolean TRUE on success * @access public @@ -598,7 +598,7 @@ class memcached /** * Sets the compression threshold * - * @param interger $thresh Threshold to compress if larger than + * @param integer $thresh Threshold to compress if larger than * * @access public */ @@ -687,7 +687,7 @@ class memcached /** * Connects $sock to $host, timing out after $timeout * - * @param interger $sock Socket to connect + * @param integer $sock Socket to connect * @param string $host Host:IP to connect to * * @return boolean @@ -807,11 +807,11 @@ class memcached // {{{ _hashfunc() /** - * Creates a hash interger based on the $key + * Creates a hash integer based on the $key * * @param string $key Key to hash * - * @return interger Hash value + * @return integer Hash value * @access private */ function _hashfunc ($key) @@ -830,9 +830,9 @@ class memcached * * @param string $cmd Command to perform * @param string $key Key to perform it on - * @param interger $amt Amount to adjust + * @param integer $amt Amount to adjust * - * @return interger New value of $key + * @return integer New value of $key * @access private */ function _incrdecr ($cmd, $key, $amt=1) @@ -929,7 +929,7 @@ class memcached * @param string $cmd Command to perform * @param string $key Key to act on * @param mixed $val What we need to store - * @param interger $exp When it should expire + * @param integer $exp When it should expire * * @return boolean * @access private @@ -1085,4 +1085,4 @@ class memcached // vim: sts=3 sw=3 et // }}} -?> + diff --git a/includes/normal/CleanUpTest.php b/includes/normal/CleanUpTest.php index cc6f0737..0ca45b3c 100644 --- a/includes/normal/CleanUpTest.php +++ b/includes/normal/CleanUpTest.php @@ -410,4 +410,4 @@ if( !$result->wasSuccessful() ) { exit( -1 ); } exit( 0 ); -?> + diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php index 9ccbc01d..aa491dbb 100644 --- a/includes/normal/RandomTest.php +++ b/includes/normal/RandomTest.php @@ -105,4 +105,4 @@ while( true ) { $norm = ''; } -?> + diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php index fc2e7776..8600d49d 100644 --- a/includes/normal/Utf8Test.php +++ b/includes/normal/Utf8Test.php @@ -150,4 +150,4 @@ function testLine( $test, $line, &$total, &$success, &$failed ) { } } -?> + diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php index 43bbafd8..557b8e5e 100644 --- a/includes/normal/UtfNormal.php +++ b/includes/normal/UtfNormal.php @@ -29,59 +29,6 @@ $utfCanonicalDecomp = NULL; global $utfCompatibilityDecomp; $utfCompatibilityDecomp = NULL; -define( 'UNICODE_HANGUL_FIRST', 0xac00 ); -define( 'UNICODE_HANGUL_LAST', 0xd7a3 ); - -define( 'UNICODE_HANGUL_LBASE', 0x1100 ); -define( 'UNICODE_HANGUL_VBASE', 0x1161 ); -define( 'UNICODE_HANGUL_TBASE', 0x11a7 ); - -define( 'UNICODE_HANGUL_LCOUNT', 19 ); -define( 'UNICODE_HANGUL_VCOUNT', 21 ); -define( 'UNICODE_HANGUL_TCOUNT', 28 ); -define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT ); - -define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 ); -define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 ); -define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 ); - -define( 'UNICODE_SURROGATE_FIRST', 0xd800 ); -define( 'UNICODE_SURROGATE_LAST', 0xdfff ); -define( 'UNICODE_MAX', 0x10ffff ); -define( 'UNICODE_REPLACEMENT', 0xfffd ); - - -define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ ); -define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ ); - -define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ ); -define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ ); -define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ ); - -define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ ); -define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ ); -define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ ); - -define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ ); -define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ ); -define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ ); -define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ ); -#define( 'UTF8_REPLACEMENT', '!' ); - -define( 'UTF8_OVERLONG_A', "\xc1\xbf" ); -define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" ); -define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" ); - -# These two ranges are illegal -define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ ); -define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ ); -define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ ); -define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); - -define( 'UTF8_HEAD', false ); -define( 'UTF8_TAIL', true ); - - /** * For using the ICU wrapper */ @@ -804,4 +751,4 @@ class UtfNormal { } } -?> + diff --git a/includes/normal/UtfNormalBench.php b/includes/normal/UtfNormalBench.php index c394f4d8..d89b0eb5 100644 --- a/includes/normal/UtfNormalBench.php +++ b/includes/normal/UtfNormalBench.php @@ -108,4 +108,4 @@ function benchmarkForm( &$u, &$data, $form ) { return $out; } -?> + diff --git a/includes/normal/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php index 30f18675..83f3085e 100644 --- a/includes/normal/UtfNormalGenerate.php +++ b/includes/normal/UtfNormalGenerate.php @@ -231,4 +231,4 @@ function callbackCompat( $matches ) { return $matches[1]; } -?> + diff --git a/includes/normal/UtfNormalTest.php b/includes/normal/UtfNormalTest.php index 6d0dce25..556cf11a 100644 --- a/includes/normal/UtfNormalTest.php +++ b/includes/normal/UtfNormalTest.php @@ -246,4 +246,4 @@ function testInvariant( &$u, $char, $desc, $reportFailure = false ) { return $result; } -?> + diff --git a/includes/normal/UtfNormalUtil.php b/includes/normal/UtfNormalUtil.php index 4ba05693..e68c6ec5 100644 --- a/includes/normal/UtfNormalUtil.php +++ b/includes/normal/UtfNormalUtil.php @@ -139,4 +139,4 @@ function escapeSingleString( $string ) { )); } -?> + diff --git a/includes/proxy_check.php b/includes/proxy_check.php index 4c672760..a878a257 100644 --- a/includes/proxy_check.php +++ b/includes/proxy_check.php @@ -51,4 +51,4 @@ $output = escapeshellarg( $output ); #`echo $output >> /home/tstarling/open/proxy.log`; -?> + diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php index e71dd396..9020c46e 100644 --- a/includes/templates/NoLocalSettings.php +++ b/includes/templates/NoLocalSettings.php @@ -7,9 +7,10 @@ if ( isset( $wgVersion ) ) { } # Set the path in case we hit a page such as /index.php/Main_Page # Could use but then we have to worry about http[s]/port #/etc. +$ext = strpos( $_SERVER['SCRIPT_NAME'], 'index.php5' ) === false ? 'php' : 'php5'; $path = ''; if( isset( $_SERVER['SCRIPT_NAME'] )) { - $path = htmlspecialchars( preg_replace('/index.php/', '', $_SERVER['SCRIPT_NAME']) ); + $path = htmlspecialchars( preg_replace('/index.php5?/', '', $_SERVER['SCRIPT_NAME']) ); } ?> @@ -39,7 +40,7 @@ if( isset( $_SERVER['SCRIPT_NAME'] )) { if ( file_exists( 'config/LocalSettings.php' ) ) { echo( 'To complete the installation, move config/LocalSettings.php to the parent directory.' ); } else { - echo( "Please set up the wiki first." ); + echo( "Please set up the wiki first." ); } ?> diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php index ccddfa66..127c30a0 100644 --- a/includes/templates/Userlogin.php +++ b/includes/templates/Userlogin.php @@ -28,11 +28,12 @@ class UserloginTemplate extends QuickTemplate {

    msg('login') ?>

    + html('header'); /* pre-table point for form plugins... */ ?>
    msgWiki('loginprompt') ?>
    haveData( 'languages' ) ) { ?>
    - - + +
    " . HTMLSelectGroups( 'member', $this->mName.'-groupsmember', $groups, true, 6 ) . "" . HTMLSelectGroups( 'available', $this->mName.'-groupsavailable', $groups, true, 6, true) . "" . $this->removeSelect( $removable ) . "" . $this->addSelect( $addable ) . "
    - + - + data['usedomain'] ) { @@ -54,7 +55,7 @@ class UserloginTemplate extends QuickTemplate { } ?> - +
    + value="" size='20' />
    msg( 'yourdomainname' ) ?>:msg( 'yourdomainname' ) ?> - + - + data['usedomain'] ) { @@ -138,7 +139,7 @@ class UsercreateTemplate extends QuickTemplate { } ?> - + - + data['useemail'] ) { ?> - + data['userealname'] ) { ?> - + @@ -200,24 +207,6 @@ class UsercreateTemplate extends QuickTemplate {
    + value="" size='20' />
    msg( 'yourdomainname' ) ?>:msg( 'yourdomainname' ) ?>
    +
    + msgWiki('prefs-help-email'); ?> +
    +
    + msgWiki('prefs-help-realname'); ?> +
    - data['userealname'] || $this->data['useemail']) { - echo '
    '; - if ( $this->data['useemail'] ) { - echo '
    '; - $this->msgHtml('prefs-help-email'); - echo '
    '; - } - if ( $this->data['userealname'] ) { - echo '
    '; - $this->msgHtml('prefs-help-realname'); - echo '
    '; - } - echo '
    '; - } - - ?> haveData( 'uselang' ) ) { ?> diff --git a/index.php b/index.php index 959fe7ff..bfbdb36a 100644 --- a/index.php +++ b/index.php @@ -93,4 +93,4 @@ $mediaWiki->finalCleanup ( $wgDeferredUpdateList, $wgLoadBalancer, $wgOut ); $mediaWiki->doUpdates( $wgPostCommitUpdateList ); $mediaWiki->restInPeace( $wgLoadBalancer ); -?> + diff --git a/index.php5 b/index.php5 new file mode 100644 index 00000000..85236494 --- /dev/null +++ b/index.php5 @@ -0,0 +1 @@ + diff --git a/install-utils.inc b/install-utils.inc index 24480f91..a9892578 100644 --- a/install-utils.inc +++ b/install-utils.inc @@ -11,7 +11,9 @@ function install_version_checks() { die( -1 ); } if( version_compare( phpversion(), '5.0.0' ) < 0 ) { - echo "PHP 5.0.0 or higher is required. ABORTING.\n"; + echo "PHP 5.0.0 or higher is required. If PHP 5 is available only when \n". + "PHP files have a .php5 extension, please navigate to index.php5 \n". + "to continue installation. ABORTING.\n"; die( -1 ); } @@ -122,4 +124,17 @@ function mw_get_session_save_path() { return $path; } -?> \ No newline at end of file +/** + * Is dl() available to us? + * + * According to http://uk.php.net/manual/en/function.dl.php, dl() + * is *not* available when `enable_dl` is off, or under `safe_mode` + * + * @return bool + */ +function mw_have_dl() { + return function_exists( 'dl' ) + && is_callable( 'dl' ) + && ini_get( 'enable_dl' ) + && !ini_get( 'safe_mode' ); +} \ No newline at end of file diff --git a/languages/Language.php b/languages/Language.php index 1eb09b67..3f9d98fb 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -59,6 +59,7 @@ class FakeConverter { class Language { var $mConverter, $mVariants, $mCode, $mLoaded = false; + var $mMagicExtensions = array(), $mMagicHookDone = false; static public $mLocalisationKeys = array( 'fallback', 'namespaceNames', 'skinNames', 'mathNames', @@ -338,9 +339,9 @@ class Language { } global $IP; - $messageFiles = glob( "$IP/languages/messages/Messages*.php" ); $names = array(); - foreach ( $messageFiles as $file ) { + $dir = opendir( "$IP/languages/messages" ); + while( false !== ( $file = readdir( $dir ) ) ) { $m = array(); if( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $file, $m ) ) { $code = str_replace( '_', '-', strtolower( $m[1] ) ); @@ -349,6 +350,7 @@ class Language { } } } + closedir( $dir ); return $names; } @@ -420,8 +422,12 @@ class Language { if ( $tz === '' ) { # Global offset in minutes. if( isset($wgLocalTZoffset) ) { - $hrDiff = $wgLocalTZoffset % 60; - $minDiff = $wgLocalTZoffset - ($hrDiff * 60); + if( $wgLocalTZoffset >= 0 ) { + $hrDiff = floor($wgLocalTZoffset / 60); + } else { + $hrDiff = ceil($wgLocalTZoffset / 60); + } + $minDiff = $wgLocalTZoffset % 60; } } elseif ( strpos( $tz, ':' ) !== false ) { $tzArray = explode( ':', $tz ); @@ -434,7 +440,8 @@ class Language { # No difference ? Return time unchanged if ( 0 == $hrDiff && 0 == $minDiff ) { return $ts; } - # Generate an adjusted date + wfSuppressWarnings(); // E_STRICT system time bitching + # Generate an adjusted date $t = mktime( ( (int)substr( $ts, 8, 2) ) + $hrDiff, # Hours (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes @@ -442,7 +449,11 @@ class Language { (int)substr( $ts, 4, 2 ), # Month (int)substr( $ts, 6, 2 ), # Day (int)substr( $ts, 0, 4 ) ); #Year - return date( 'YmdHis', $t ); + + $date = date( 'YmdHis', $t ); + wfRestoreWarnings(); + + return $date; } /** @@ -468,6 +479,9 @@ class Language { * i's" => 20'11" * * Backslash escaping is also supported. + * + * Input timestamp is assumed to be pre-normalized to the desired local + * time zone, if any. * * @param string $format * @param string $ts 14-character timestamp @@ -508,31 +522,31 @@ class Language { break; case 'D': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $s .= $this->getWeekdayAbbreviation( date( 'w', $unix ) + 1 ); + $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) + 1 ); break; case 'j': $num = intval( substr( $ts, 6, 2 ) ); break; case 'l': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $s .= $this->getWeekdayName( date( 'w', $unix ) + 1 ); + $s .= $this->getWeekdayName( gmdate( 'w', $unix ) + 1 ); break; case 'N': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $w = date( 'w', $unix ); + $w = gmdate( 'w', $unix ); $num = $w ? $w : 7; break; case 'w': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $num = date( 'w', $unix ); + $num = gmdate( 'w', $unix ); break; case 'z': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $num = date( 'z', $unix ); + $num = gmdate( 'z', $unix ); break; case 'W': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $num = date( 'W', $unix ); + $num = gmdate( 'W', $unix ); break; case 'F': $s .= $this->getMonthName( substr( $ts, 4, 2 ) ); @@ -548,11 +562,11 @@ class Language { break; case 't': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $num = date( 't', $unix ); + $num = gmdate( 't', $unix ); break; case 'L': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $num = date( 'L', $unix ); + $num = gmdate( 'L', $unix ); break; case 'Y': $num = substr( $ts, 0, 4 ); @@ -588,11 +602,11 @@ class Language { break; case 'c': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $s .= date( 'c', $unix ); + $s .= gmdate( 'c', $unix ); break; case 'r': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); - $s .= date( 'r', $unix ); + $s .= gmdate( 'r', $unix ); break; case 'U': if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts ); @@ -1115,8 +1129,8 @@ class Language { # Fill a MagicWord object with data from here function getMagic( &$mw ) { - if ( !isset( $this->mMagicExtensions ) ) { - $this->mMagicExtensions = array(); + if ( !$this->mMagicHookDone ) { + $this->mMagicHookDone = true; wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) ); } if ( isset( $this->mMagicExtensions[$mw->mId] ) ) { @@ -1139,6 +1153,24 @@ class Language { $mw->mSynonyms = array_slice( $rawEntry, 1 ); } + /** + * Add magic words to the extension array + */ + function addMagicWordsByLang( $newWords ) { + $code = $this->getCode(); + $fallbackChain = array(); + while ( $code && !in_array( $code, $fallbackChain ) ) { + $fallbackChain[] = $code; + $code = self::getFallbackFor( $code ); + } + $fallbackChain = array_reverse( $fallbackChain ); + foreach ( $fallbackChain as $code ) { + if ( isset( $newWords[$code] ) ) { + $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions; + } + } + } + /** * Get special page names, as an associative array * case folded alias => real name @@ -1258,13 +1290,21 @@ class Language { return $s; } - # Crop a string from the beginning or end to a certain number of bytes. - # (Bytes are used because our storage has limited byte lengths for some - # columns in the database.) Multibyte charsets will need to make sure that - # only whole characters are included! - # - # $length does not include the optional ellipsis. - # If $length is negative, snip from the beginning + /** + * Truncate a string to a specified length in bytes, appending an optional + * string (e.g. for ellipses) + * + * The database offers limited byte lengths for some columns in the database; + * multi-byte character sets mean we need to ensure that only whole characters + * are included, otherwise broken characters can be passed to the user + * + * If $length is negative, the string will be truncated from the beginning + * + * @param string $string String to truncate + * @param int $length Maximum length (excluding ellipses) + * @param string $ellipses String to append to the truncated text + * @return string + */ function truncate( $string, $length, $ellipsis = "" ) { if( $length == 0 ) { return $ellipsis; @@ -1811,6 +1851,73 @@ class Language { wfProfileOut( __METHOD__ ); return array( $wikiUpperChars, $wikiLowerChars ); } + + function formatTimePeriod( $seconds ) { + if ( $seconds < 10 ) { + return $this->formatNum( sprintf( "%.1f", $seconds ) ) . wfMsg( 'seconds-abbrev' ); + } elseif ( $seconds < 60 ) { + return $this->formatNum( round( $seconds ) ) . wfMsg( 'seconds-abbrev' ); + } elseif ( $seconds < 3600 ) { + return $this->formatNum( floor( $seconds / 60 ) ) . wfMsg( 'minutes-abbrev' ) . + $this->formatNum( round( fmod( $seconds, 60 ) ) ) . wfMsg( 'seconds-abbrev' ); + } else { + $hours = floor( $seconds / 3600 ); + $minutes = floor( ( $seconds - $hours * 3600 ) / 60 ); + $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 ); + return $this->formatNum( $hours ) . wfMsg( 'hours-abbrev' ) . + $this->formatNum( $minutes ) . wfMsg( 'minutes-abbrev' ) . + $this->formatNum( $secondsPart ) . wfMsg( 'seconds-abbrev' ); + } + } + + function formatBitrate( $bps ) { + $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' ); + if ( $bps <= 0 ) { + return $this->formatNum( $bps ) . $units[0]; + } + $unitIndex = floor( log10( $bps ) / 3 ); + $mantissa = $bps / pow( 1000, $unitIndex ); + if ( $mantissa < 10 ) { + $mantissa = round( $mantissa, 1 ); + } else { + $mantissa = round( $mantissa ); + } + return $this->formatNum( $mantissa ) . $units[$unitIndex]; + } + + /** + * Format a size in bytes for output, using an appropriate + * unit (B, KB, MB or GB) according to the magnitude in question + * + * @param $size Size to format + * @return string Plain text (not HTML) + */ + function formatSize( $size ) { + // For small sizes no decimal places necessary + $round = 0; + if( $size > 1024 ) { + $size = $size / 1024; + if( $size > 1024 ) { + $size = $size / 1024; + // For MB and bigger two decimal places are smarter + $round = 2; + if( $size > 1024 ) { + $size = $size / 1024; + $msg = 'size-gigabytes'; + } else { + $msg = 'size-megabytes'; + } + } else { + $msg = 'size-kilobytes'; + } + } else { + $msg = 'size-bytes'; + } + $size = round( $size, $round ); + $text = $this->getMessageFromDB( $msg ); + return str_replace( '$1', $this->formatNum( $size ), $text ); + } } -?> + + diff --git a/languages/LanguageConverter.php b/languages/LanguageConverter.php index d352f645..43f33ae6 100644 --- a/languages/LanguageConverter.php +++ b/languages/LanguageConverter.php @@ -777,7 +777,7 @@ class LanguageConverter { * MediaWiki:conversiontable* is updated * @private */ - function OnArticleSaveComplete($article, $user, $text, $summary, $isminor, $iswatch, $section) { + function OnArticleSaveComplete($article, $user, $text, $summary, $isminor, $iswatch, $section, $flags, $revision) { $titleobj = $article->getTitle(); if($titleobj->getNamespace() == NS_MEDIAWIKI) { /* @@ -809,4 +809,4 @@ class LanguageConverter { } -?> + diff --git a/languages/Names.php b/languages/Names.php index 34041747..722822c7 100644 --- a/languages/Names.php +++ b/languages/Names.php @@ -23,9 +23,11 @@ 'az' => 'Azərbaycan', # Azerbaijani 'ba' => 'Башҡорт', # Bashkir 'bar' => 'Boarisch', # Bavarian (Austro-Bavarian and South Tyrolean) - 'bat-smg' => 'Žemaitėška', # Samogitian + 'bat-smg' => 'Žemaitėška', # Samogitian + 'bcl' => 'Bikol Central', # Bikol: Central Bicolano language 'be' => 'Беларуская', # Belarusian normative - 'be-x-old' => 'Беларуская', # Belarusian alternative + 'be-tarask' => 'Беларуская (тарашкевіца)', # Belarusian in Taraskievica orthography + 'be-x-old' => 'Беларуская (тарашкевіца)', # Belarusian in Taraskievica orthography; compat link 'bg' => 'Български', # Bulgarian 'bh' => 'भोजपुरी', # Bihara 'bi' => 'Bislama', # Bislama @@ -70,6 +72,7 @@ 'es' => 'Español', # Spanish 'et' => 'Eesti', # Estonian 'eu' => 'Euskara', # Basque + 'ext' => 'Estremeñu', # Extremaduran 'fa' => 'فارسی', # Persian 'ff' => 'Fulfulde', # Fulah 'fi' => 'Suomi', # Finnish @@ -77,12 +80,13 @@ 'fj' => 'Na Vosa Vakaviti', # Fijian 'fo' => 'Føroyskt', # Faroese 'fr' => 'Français', # French - 'frp' => 'Arpitan', # Franco-Provençal/Arpitan + 'frc' => 'Français cadien', # Cajun French + 'frp' => 'Arpetan', # Franco-Provençal/Arpitan 'fur' => 'Furlan', # Friulian 'fy' => 'Frysk', # Frisian 'ga' => 'Gaeilge', # Irish 'gd' => 'Gàidhlig', # Scots Gaelic - 'gl' => 'Galego', # Gallegan + 'gl' => 'Galego', # Galician 'glk' => 'گیلکی', # Gilaki 'gn' => 'Avañe\'ẽ', # Guarani 'got' => '𐌲𐌿𐍄𐌹𐍃𐌺', # Gothic @@ -90,6 +94,7 @@ 'gu' => 'ગુજરાતી', # Gujarati 'gv' => 'Gaelg', # Manx 'ha' => 'هَوُسَ', # Hausa + 'hak' => 'Hak-kâ-fa', # Hakka 'haw' => 'Hawai`i', # Hawaiian 'he' => 'עברית', # Hebrew 'hi' => 'हिन्दी', # Hindi @@ -97,7 +102,7 @@ 'ho' => 'Hiri Motu', # Hiri Motu 'hr' => 'Hrvatski', # Croatian 'hsb' => 'Hornjoserbsce', # Upper Sorbian - 'ht' => 'Krèyol ayisyen', # Haitian, common/popular form is Kreyòl + 'ht' => 'Kreyòl ayisyen', # Haitian 'hu' => 'Magyar', # Hungarian 'hy' => 'Հայերեն', # Armenian 'hz' => 'Otsiherero', # Herero @@ -130,11 +135,12 @@ 'kn' => 'ಕನ್ನಡ', # Kannada 'ko' => '한국어', # Korean 'kr' => 'Kanuri', # Kanuri (FIXME!) + 'krj' => 'Kinaray-a', # Kinaray-a 'ks' => 'कश्मीरी - (كشميري)', # Kashmiri 'ksh' => 'Ripoarisch', # Ripuarian - 'ksh-c-a' => 'Ripoarisch c a', # Ripuarian - 'ksh-p-b' => 'Ripoarisch p b', # Ripuarian - 'ku' => 'Kurdî / كوردي', # Kurdish + 'ku' => 'Kurdî / كوردی', # Kurdish + 'ku-latn' => "\xE2\x80\xAAKurdî (latînî)\xE2\x80\xAC", # Kurdish Latin script + 'ku-arab' => "\xE2\x80\xABكوردي (عەرەبی)\xE2\x80\xAC", # Kurdish Arabic script 'kv' => 'Коми', # Komi, cyrillic is common script but also written in latin script 'kw' => 'Kernewek', # Cornish 'ky' => 'Кыргызча', # Kirghiz @@ -206,12 +212,13 @@ 'rmy' => 'Romani', # Vlax Romany 'rn' => 'Kirundi', # Kirundi 'ro' => 'Română', # Romanian - 'roa-rup' => 'Armâneashti', # Aromanian + 'roa-rup' => 'Armãneashce', # Aromanian 'roa-tara' => 'Tarandíne', # Tarantino 'ru' => 'Русский', # Russian 'ru-sib' => 'Сибирской', # Siberian/North Russian 'rw' => 'Kinyarwanda', # Kinyarwanda, should possibly be Kinyarwandi 'sa' => 'संस्कृत', # Sanskrit + 'sah' => 'Саха тыла', # Sakha 'sc' => 'Sardu', # Sardinian 'scn' => 'Sicilianu', # Sicilian 'sco' => 'Scots', # Scots @@ -259,7 +266,7 @@ 'ty' => 'Reo Mā`ohi', # Tahitian 'tyv' => 'Тыва дыл', # Tyvan 'udm' => 'Удмурт', # Udmurt - 'ug' => 'Oyghurque', # Uyghur + 'ug' => 'Uyghurche‎ / ئۇيغۇرچە', # Uyghur 'uk' => 'Українська', # Ukrainian 'ur' => 'اردو', # Urdu 'uz' => 'O\'zbek', # Uzbek @@ -292,4 +299,4 @@ 'zh-yue' => '粵語', # Cantonese -- (see bug 8217) 'zu' => 'isiZulu' # Zulu ); -?> + diff --git a/languages/classes/LanguageAr.php b/languages/classes/LanguageAr.php index c8824814..514ff028 100644 --- a/languages/classes/LanguageAr.php +++ b/languages/classes/LanguageAr.php @@ -25,4 +25,3 @@ class LanguageAr extends Language { } -?> \ No newline at end of file diff --git a/languages/classes/LanguageAz.php b/languages/classes/LanguageAz.php index 399014c8..84f0c7dd 100644 --- a/languages/classes/LanguageAz.php +++ b/languages/classes/LanguageAz.php @@ -13,4 +13,4 @@ class LanguageAz extends Language { } } -?> + diff --git a/languages/classes/LanguageBe.php b/languages/classes/LanguageBe.php index d27e2835..e0665e68 100644 --- a/languages/classes/LanguageBe.php +++ b/languages/classes/LanguageBe.php @@ -88,4 +88,4 @@ class LanguageBe extends Language { } -?> + diff --git a/languages/classes/LanguageBe_tarask.php b/languages/classes/LanguageBe_tarask.php new file mode 100644 index 00000000..5e168b7e --- /dev/null +++ b/languages/classes/LanguageBe_tarask.php @@ -0,0 +1,88 @@ + + * @bug 1638, 2135 + * @link http://be.wikipedia.org/wiki/Talk:LanguageBe.php + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License + * @license http://www.gnu.org/copyleft/fdl.html GNU Free Documentation License + */ + +class LanguageBe_tarask extends Language { + function convertPlural( $count, $wordform1, $wordform2, $wordform3, $w4, $w5) { + $count = str_replace ('.', '', $count); + if ($count > 10 && floor(($count % 100) / 10) == 1) { + return $wordform3; + } else { + switch ($count % 10) { + case 1: return $wordform1; + case 2: + case 3: + case 4: return $wordform2; + default: return $wordform3; + } + } + } + + # Convert from the nominative form of a noun to some other case + # Invoked with {{GRAMMAR:case|word}} + /** + * Cases: родны, вінавальны, месны + */ + function convertGrammar( $word, $case ) { + switch ( $case ) { + case 'родны': # genitive + if ( $word == 'Вікіпэдыя' ) { + $word = 'Вікіпэдыі'; + } elseif ( $word == 'ВікіСлоўнік' ) { + $word = 'ВікіСлоўніка'; + } elseif ( $word == 'ВікіКнігі' ) { + $word = 'ВікіКніг'; + } elseif ( $word == 'ВікіКрыніца' ) { + $word = 'ВікіКрыніцы'; + } elseif ( $word == 'ВікіНавіны' ) { + $word = 'ВікіНавін'; + } elseif ( $word == 'ВікіВіды' ) { + $word = 'ВікіВідаў'; + } + break; + case 'вінавальны': # akusative + if ( $word == 'Вікіпэдыя' ) { + $word = 'Вікіпэдыю'; + } elseif ( $word == 'ВікіСлоўнік' ) { + $word = 'ВікіСлоўнік'; + } elseif ( $word == 'ВікіКнігі' ) { + $word = 'ВікіКнігі'; + } elseif ( $word == 'ВікіКрыніца' ) { + $word = 'ВікіКрыніцу'; + } elseif ( $word == 'ВікіНавіны' ) { + $word = 'ВікіНавіны'; + } elseif ( $word == 'ВікіВіды' ) { + $word = 'ВікіВіды'; + } + break; + case 'месны': # prepositional + if ( $word == 'Вікіпэдыя' ) { + $word = 'Вікіпэдыі'; + } elseif ( $word == 'ВікіСлоўнік' ) { + $word = 'ВікіСлоўніку'; + } elseif ( $word == 'ВікіКнігі' ) { + $word = 'ВікіКнігах'; + } elseif ( $word == 'ВікіКрыніца' ) { + $word = 'ВікіКрыніцы'; + } elseif ( $word == 'ВікіНавіны' ) { + $word = 'ВікіНавінах'; + } elseif ( $word == 'ВікіВіды' ) { + $word = 'ВікіВідах'; + } + break; + } + + return $word; # this will return the original value for 'назоўны' (nominative) and all undefined case values + } + +} + + diff --git a/languages/classes/LanguageBg.php b/languages/classes/LanguageBg.php index 576c9385..bf3ad5c8 100644 --- a/languages/classes/LanguageBg.php +++ b/languages/classes/LanguageBg.php @@ -20,4 +20,4 @@ class LanguageBg extends Language { } } } -?> + diff --git a/languages/classes/LanguageBs.php b/languages/classes/LanguageBs.php index 6dbcbfa1..119280e6 100644 --- a/languages/classes/LanguageBs.php +++ b/languages/classes/LanguageBs.php @@ -133,4 +133,4 @@ class LanguageBs extends Language { } -?> + diff --git a/languages/classes/LanguageCs.php b/languages/classes/LanguageCs.php index eff04a00..ac80f296 100644 --- a/languages/classes/LanguageCs.php +++ b/languages/classes/LanguageCs.php @@ -83,4 +83,4 @@ class LanguageCs extends Language { } } -?> + diff --git a/languages/classes/LanguageCu.php b/languages/classes/LanguageCu.php index 1da7c699..61128f24 100644 --- a/languages/classes/LanguageCu.php +++ b/languages/classes/LanguageCu.php @@ -46,4 +46,4 @@ class LanguageCu extends Language { } } -?> + diff --git a/languages/classes/LanguageEo.php b/languages/classes/LanguageEo.php index 11b0e987..f5a09897 100644 --- a/languages/classes/LanguageEo.php +++ b/languages/classes/LanguageEo.php @@ -70,4 +70,4 @@ class LanguageEo extends Language { } } -?> + diff --git a/languages/classes/LanguageEt.php b/languages/classes/LanguageEt.php index 1fc44fc0..8ed494f8 100644 --- a/languages/classes/LanguageEt.php +++ b/languages/classes/LanguageEt.php @@ -17,4 +17,4 @@ class LanguageEt extends Language { } } } -?> + diff --git a/languages/classes/LanguageFi.php b/languages/classes/LanguageFi.php index d7363969..0d02dfe7 100644 --- a/languages/classes/LanguageFi.php +++ b/languages/classes/LanguageFi.php @@ -10,7 +10,7 @@ class LanguageFi extends Language { * Avoid grouping whole numbers between 0 to 9999 */ function commafy($_) { - if (!preg_match('/^\d{1,4}$/',$_)) { + if (!preg_match('/^-?\d{1,4}$/',$_)) { return strrev((string)preg_replace('/(\d{3})(?=\d)(?!\d*\.)/','$1,',strrev($_))); } else { return $_; @@ -64,7 +64,7 @@ class LanguageFi extends Language { return $word; } - function translateBlockExpiry( $str ) { + function translateBlockExpiry( $str, $forContent = false ) { /* 'ago', 'now', 'today', 'this', 'next', 'first', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth', 'eleventh', 'twelfth', @@ -138,9 +138,14 @@ class LanguageFi extends Language { $final .= ' ' . $item; } - return '”' . trim( $final ) . '”'; + + if ( $forContent ) { + return htmlspecialchars( trim( $final ) ); + } else { + return '”' . trim( $final ) . '”'; + } } } -?> + diff --git a/languages/classes/LanguageFr.php b/languages/classes/LanguageFr.php index ecc15317..87d28b22 100644 --- a/languages/classes/LanguageFr.php +++ b/languages/classes/LanguageFr.php @@ -13,4 +13,4 @@ class LanguageFr extends Language { return $count <= '1' ? $w1 : $w2; } } -?> + diff --git a/languages/classes/LanguageGa.php b/languages/classes/LanguageGa.php index b0a5978e..71b56e72 100644 --- a/languages/classes/LanguageGa.php +++ b/languages/classes/LanguageGa.php @@ -48,4 +48,4 @@ class LanguageGa extends Language { } -?> + diff --git a/languages/classes/LanguageGsw.php b/languages/classes/LanguageGsw.php index f8ec0c80..0ee043dc 100644 --- a/languages/classes/LanguageGsw.php +++ b/languages/classes/LanguageGsw.php @@ -65,4 +65,4 @@ class LanguageGsw extends Language { } -?> + diff --git a/languages/classes/LanguageHe.php b/languages/classes/LanguageHe.php index 8e503fa6..ca760c1a 100644 --- a/languages/classes/LanguageHe.php +++ b/languages/classes/LanguageHe.php @@ -68,4 +68,4 @@ class LanguageHe extends Language { } } -?> + diff --git a/languages/classes/LanguageHr.php b/languages/classes/LanguageHr.php index 48908735..5a875a71 100644 --- a/languages/classes/LanguageHr.php +++ b/languages/classes/LanguageHr.php @@ -22,4 +22,4 @@ class LanguageHr extends Language { } -?> + diff --git a/languages/classes/LanguageHu.php b/languages/classes/LanguageHu.php index 0dc4ac4c..e4407f5b 100644 --- a/languages/classes/LanguageHu.php +++ b/languages/classes/LanguageHu.php @@ -49,4 +49,4 @@ class LanguageHu extends Language { } } -?> + diff --git a/languages/classes/LanguageHy.php b/languages/classes/LanguageHy.php index b3a46b0f..34fb6c74 100644 --- a/languages/classes/LanguageHy.php +++ b/languages/classes/LanguageHy.php @@ -70,4 +70,4 @@ class LanguageHy extends Language { } } } -?> + diff --git a/languages/classes/LanguageJa.php b/languages/classes/LanguageJa.php index 7f681639..c56e6a9e 100644 --- a/languages/classes/LanguageJa.php +++ b/languages/classes/LanguageJa.php @@ -38,4 +38,4 @@ class LanguageJa extends Language { } } -?> + diff --git a/languages/classes/LanguageKk.deps.php b/languages/classes/LanguageKk.deps.php index 22ad7ad5..336692a2 100644 --- a/languages/classes/LanguageKk.deps.php +++ b/languages/classes/LanguageKk.deps.php @@ -9,4 +9,4 @@ require_once( dirname(__FILE__).'/../LanguageConverter.php' ); require_once( dirname(__FILE__).'/LanguageKk_kz.php' ); -?> + diff --git a/languages/classes/LanguageKk.php b/languages/classes/LanguageKk.php index 3591d11a..68775b3a 100644 --- a/languages/classes/LanguageKk.php +++ b/languages/classes/LanguageKk.php @@ -101,6 +101,23 @@ class KkConverter extends LanguageConverter { ); } + /* rules should be defined as -{ekavian | iyekavian-} -or- + -{code:text | code:text | ...}- + update: delete all rule parsing because it's not used + currently, and just produces a couple of bugs + */ + function parseManualRule($rule, $flags=array()) { + if(in_array('T',$flags)){ + return parent::parseManualRule($rule, $flags); + } + + // otherwise ignore all formatting + foreach($this->mVariants as $v) { + $carray[$v] = $rule; + } + + return $carray; + } // Do not convert content on talk pages function parserConvert( $text, &$parser ){ @@ -195,9 +212,9 @@ class LanguageKk extends LanguageKk_kz { $variants = array( 'kk', 'kk-kz', 'kk-tr', 'kk-cn' ); $variantfallbacks = array( 'kk' => 'kk-kz', - 'kk-kz' => 'kk-kz', - 'kk-tr' => 'kk-tr', - 'kk-cn' => 'kk-cn' + 'kk-kz' => 'kk', + 'kk-tr' => 'kk', + 'kk-cn' => 'kk' ); $this->mConverter = new KkConverter( $this, 'kk', $variants, $variantfallbacks ); @@ -223,4 +240,4 @@ class LanguageKk extends LanguageKk_kz { } -?> + diff --git a/languages/classes/LanguageKk_kz.php b/languages/classes/LanguageKk_kz.php index d1f4bfd2..8783ea6f 100644 --- a/languages/classes/LanguageKk_kz.php +++ b/languages/classes/LanguageKk_kz.php @@ -265,4 +265,4 @@ class LanguageKk_kz extends Language { } } -?> + diff --git a/languages/classes/LanguageKo.php b/languages/classes/LanguageKo.php index ae09d111..1ed80dfe 100644 --- a/languages/classes/LanguageKo.php +++ b/languages/classes/LanguageKo.php @@ -54,4 +54,4 @@ class LanguageKo extends Language { } } -?> + diff --git a/languages/classes/LanguageKsh.php b/languages/classes/LanguageKsh.php index b15f97e3..c53667cf 100644 --- a/languages/classes/LanguageKsh.php +++ b/languages/classes/LanguageKsh.php @@ -32,4 +32,4 @@ class LanguageKsh extends Language { } } } -?> + diff --git a/languages/classes/LanguageKu.deps.php b/languages/classes/LanguageKu.deps.php new file mode 100644 index 00000000..7aeba604 --- /dev/null +++ b/languages/classes/LanguageKu.deps.php @@ -0,0 +1,12 @@ + 'b', 'ج' => 'c', 'چ' => 'ç', 'د' => 'd', 'ف' => 'f', 'گ' => 'g', 'ھ' => 'h', + 'ہ' => 'h', 'ه' => 'h', 'ح' => 'h', 'ژ' => 'j', 'ك' => 'k', 'ک' => 'k', 'ل' => 'l', + 'م' => 'm', 'ن' => 'n', 'پ' => 'p', 'ق' => 'q', 'ر' => 'r', 'س' => 's', 'ش' => 'ş', + 'ت' => 't', 'ڤ' => 'v', 'خ' => 'x', 'غ' => 'x', 'ز' => 'z', + + /* Doppel- und Halbvokale */ + 'ڵ' => 'll', #ll + 'ڕ' => 'rr', #rr + 'ا' => 'a', + # 'ئێ' => 'ê', # initial e + 'ە' => 'e', + 'ه‌' => 'e', # with one non-joiner + 'ه‌‌' => 'e', # with two non-joiner + 'ة' => 'e', + 'ێ' => 'ê', + 'ي' => 'î', + 'ی' => 'î', # U+06CC db 8c ARABIC LETTER FARSI YEH + 'ى' => 'î', # U+0649 d9 89 ARABIC LETTER ALEF MAKSURA + 'ۆ' => 'o', + 'و' => 'w', + 'ئ' => '', # initial hemze should not be shown + '،' => ',', + 'ع' => '\'', # ayn + '؟' => '?', + ); + + var $mLatinToArabic = array( + 'b' => 'ب', 'c' => 'ج', 'ç' => 'چ', 'd' => 'د', 'f' => 'ف', 'g' => 'گ', + 'h' => 'ه', 'j' => 'ژ', 'k' => 'ک', 'l' => 'ل', + 'm' => 'م', 'n' => 'ن', 'p' => 'پ', 'q' => 'ق', 'r' => 'ر', 's' => 'س', 'ş' => 'ش', + 't' => 'ت', 'v' => 'ڤ', + 'x' => 'خ', 'y' => 'ی', 'z' => 'ز', + + + 'B' => 'ب', 'C' => 'ج', 'Ç' => 'چ', 'D' => 'د', 'F' => 'ف', 'G' => 'گ', 'H' => 'ھ', + 'H' => 'ہ', 'H' => 'ه', 'H' => 'ح', 'J' => 'ژ', 'K' => 'ك', 'K' => 'ک', 'L' => 'ل', + 'M' => 'م', 'N' => 'ن', 'P' => 'پ', 'Q' => 'ق', 'R' => 'ر', 'S' => 'س', 'Ş' => 'ش', + 'T' => 'ت', 'V' => 'ڤ', 'W' => 'و', 'X' => 'خ', + 'Y' => 'ی', 'Z' => 'ز', + + /* Doppelkonsonanten */ + # 'll' => 'ڵ', # wenn es geht, doppel-l und l getrennt zu behandeln + # 'rr' => 'ڕ', # selbiges für doppel-r + + /* Einzelne Großbuchstaben */ + //' C' => 'ج', + + /* Vowels */ + 'a' => 'ا', + 'e' => 'ە', + 'ê' => 'ێ', + 'i' => '', + 'î' => 'ی', + 'o' => 'ۆ', + 'u' => 'و', + 'û' => 'وو', + 'w' => 'و', + ',' => '،', + '?' => '؟', + + # Try to replace the leading vowel + ' a' => 'ئا ', + ' e' => 'ئە ', + ' ê' => 'ئێ ', + ' î' => 'ئی ', + ' o' => 'ئۆ ', + ' u' => 'ئو ', + ' û' => 'ئوو ', + 'A' => 'ئا', + 'E' => 'ئە', + 'Ê' => 'ئێ', + 'Î' => 'ئی', + 'O' => 'ئۆ', + 'U' => 'ئو', + 'Û' => 'ئوو', + ' A' => 'ئا ', + ' E' => 'ئە ', + ' Ê' => 'ئێ ', + ' Î' => 'ئی ', + ' O' => 'ئۆ ', + ' U' => 'ئو ', + ' Û' => 'ئوو ', + # eyn erstmal deaktivieren, einfache Anführungsstriche sind einfach zu häufig, um sie als eyn zu interpretieren + # '\'' => 'ع', + + ); + + function loadDefaultTables() { + $this->mTables = array( + 'ku-latn' => new ReplacementArray( $this->mArabicToLatin ), + 'ku-arab' => new ReplacementArray( $this->mLatinToArabic ), + 'ku' => new ReplacementArray() + ); + } + + + // Do not convert content on talk pages + function parserConvert( $text, &$parser ){ + if(is_object($parser->getTitle() ) && $parser->getTitle()->isTalkPage()) + $this->mDoContentConvert=false; + else + $this->mDoContentConvert=true; + + return parent::parserConvert($text, $parser ); + } + + /* + * A function wrapper: + * - if there is no selected variant, leave the link + * names as they were + * - do not try to find variants for usernames + */ + function findVariantLink( &$link, &$nt ) { + // check for user namespace + if(is_object($nt)){ + $ns = $nt->getNamespace(); + if($ns==NS_USER || $ns==NS_USER_TALK) + return; + } + + $oldlink=$link; + parent::findVariantLink($link,$nt); + if($this->getPreferredVariant()==$this->mMainLanguageCode) + $link=$oldlink; + } + + /* + * We want our external link captions to be converted in variants, + * so we return the original text instead -{$text}-, except for URLs + */ + function markNoConversion($text, $noParse=false) { + if($noParse || preg_match("/^https?:\/\/|ftp:\/\/|irc:\/\//",$text)) + return parent::markNoConversion($text); + return $text; + } + + /* + * An ugly function wrapper for parsing Image titles + * (to prevent image name conversion) + */ + function autoConvert($text, $toVariant=false) { + global $wgTitle; + if(is_object($wgTitle) && $wgTitle->getNameSpace()==NS_IMAGE){ + $imagename = $wgTitle->getNsText(); + if(preg_match("/^$imagename:/",$text)) return $text; + } + return parent::autoConvert($text,$toVariant); + } + + /** + * It translates text into variant, specials: + * - ommiting roman numbers + */ + function translate($text, $toVariant){ + $breaks = '[^\w\x80-\xff]'; + + /* From Kazakh interface, maybe we need it later + * + // regexp for roman numbers + $roman = 'M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})'; + $roman = ''; + + $reg = '/^'.$roman.'$|^'.$roman.$breaks.'|'.$breaks.$roman.'$|'.$breaks.$roman.$breaks.'/'; + + $matches = preg_split($reg, $text, -1, PREG_SPLIT_OFFSET_CAPTURE); + + $m = array_shift($matches); + if( !isset( $this->mTables[$toVariant] ) ) { + throw new MWException( "Broken variant table: " . implode( ',', array_keys( $this->mTables ) ) ); + } + $ret = $this->mTables[$toVariant]->replace( $m[0] ); + $mstart = $m[1]+strlen($m[0]); + foreach($matches as $m) { + $ret .= substr($text, $mstart, $m[1]-$mstart); + $ret .= parent::translate($m[0], $toVariant); + $mstart = $m[1] + strlen($m[0]); + } + + return $ret; + */ + + if( !isset( $this->mTables[$toVariant] ) ) { + throw new MWException( "Broken variant table: " . implode( ',', array_keys( $this->mTables ) ) ); + } + + return parent::translate( $text, $toVariant ); + } +} + +class LanguageKu extends LanguageKu_ku { + + function __construct() { + global $wgHooks; + parent::__construct(); + + $variants = array( 'ku', 'ku-arab', 'ku-latn' ); + $variantfallbacks = array( + 'ku' => 'ku-latn', + 'ku-arab' => 'ku-latn', + 'ku-latn' => 'ku-arab', + ); + + $this->mConverter = new KuConverter( $this, 'ku', $variants, $variantfallbacks ); + $wgHooks['ArticleSaveComplete'][] = $this->mConverter; + } + +/* From Kazakh interface, not needed for the moment + + function convertGrammar( $word, $case ) { + $fname="LanguageKu::convertGrammar"; + wfProfileIn( $fname ); + + //always convert to ku-latn before convertGrammar + $w1 = $word; + $word = $this->mConverter->autoConvert( $word, 'ku-latn' ); + $w2 = $word; + $word = parent::convertGrammar( $word, $case ); + //restore encoding + if( $w1 != $w2 ) { + $word = $this->mConverter->translate( $word, 'ku-latn' ); + } + wfProfileOut( $fname ); + return $word; + } +*/ +} + + diff --git a/languages/classes/LanguageKu_ku.php b/languages/classes/LanguageKu_ku.php new file mode 100644 index 00000000..d8be6d9b --- /dev/null +++ b/languages/classes/LanguageKu_ku.php @@ -0,0 +1,35 @@ + + diff --git a/languages/classes/LanguageLt.php b/languages/classes/LanguageLt.php index b09520c5..f2b29cad 100644 --- a/languages/classes/LanguageLt.php +++ b/languages/classes/LanguageLt.php @@ -18,4 +18,4 @@ class LanguageLt extends Language { return empty($wordform3)?$wordform2:$wordform3; } } -?> + diff --git a/languages/classes/LanguageLv.php b/languages/classes/LanguageLv.php index ef1f6237..8a46bb9d 100644 --- a/languages/classes/LanguageLv.php +++ b/languages/classes/LanguageLv.php @@ -53,4 +53,4 @@ class LanguageLv extends Language { } -?> + diff --git a/languages/classes/LanguagePt_br.php b/languages/classes/LanguagePt_br.php index 813c9ad6..31574ed8 100644 --- a/languages/classes/LanguagePt_br.php +++ b/languages/classes/LanguagePt_br.php @@ -13,4 +13,4 @@ class LanguagePt_br extends Language { return $count <= '1' ? $w1 : $w2; } } -?> + diff --git a/languages/classes/LanguageRmy.php b/languages/classes/LanguageRmy.php index bbf22d52..e6096348 100644 --- a/languages/classes/LanguageRmy.php +++ b/languages/classes/LanguageRmy.php @@ -69,4 +69,4 @@ class LanguageRmy extends Language { } } -?> + diff --git a/languages/classes/LanguageRu.php b/languages/classes/LanguageRu.php index 3ca5fd65..7d088053 100644 --- a/languages/classes/LanguageRu.php +++ b/languages/classes/LanguageRu.php @@ -83,4 +83,4 @@ class LanguageRu extends Language { } } } -?> + diff --git a/languages/classes/LanguageSk.php b/languages/classes/LanguageSk.php index 06f44a33..1b9832cc 100644 --- a/languages/classes/LanguageSk.php +++ b/languages/classes/LanguageSk.php @@ -90,4 +90,4 @@ class LanguageSk extends Language { } } -?> + diff --git a/languages/classes/LanguageSl.php b/languages/classes/LanguageSl.php index e8f94567..2513d6bd 100644 --- a/languages/classes/LanguageSl.php +++ b/languages/classes/LanguageSl.php @@ -90,4 +90,4 @@ class LanguageSl extends Language { } -?> + diff --git a/languages/classes/LanguageSr.deps.php b/languages/classes/LanguageSr.deps.php index 8fe354e0..018a83c2 100644 --- a/languages/classes/LanguageSr.deps.php +++ b/languages/classes/LanguageSr.deps.php @@ -7,4 +7,4 @@ require_once( dirname(__FILE__).'/LanguageSr_ec.php' ); require_once( dirname(__FILE__).'/../LanguageConverter.php' ); -?> + diff --git a/languages/classes/LanguageSr.php b/languages/classes/LanguageSr.php index 07982658..7fe67941 100644 --- a/languages/classes/LanguageSr.php +++ b/languages/classes/LanguageSr.php @@ -188,4 +188,4 @@ class LanguageSr extends LanguageSr_ec { $wgHooks['ArticleSaveComplete'][] = $this->mConverter; } } -?> + diff --git a/languages/classes/LanguageSr_ec.php b/languages/classes/LanguageSr_ec.php index 914080da..42647ae6 100644 --- a/languages/classes/LanguageSr_ec.php +++ b/languages/classes/LanguageSr_ec.php @@ -24,4 +24,4 @@ class LanguageSr_ec extends Language { } } -?> + diff --git a/languages/classes/LanguageSr_el.deps.php b/languages/classes/LanguageSr_el.deps.php index f39da2f2..d73638f4 100644 --- a/languages/classes/LanguageSr_el.deps.php +++ b/languages/classes/LanguageSr_el.deps.php @@ -6,4 +6,4 @@ // see http://mail.wikipedia.org/pipermail/wikitech-l/2006-January/033660.html require_once( dirname(__FILE__).'/LanguageSr_ec.php' ); -?> + diff --git a/languages/classes/LanguageSr_el.php b/languages/classes/LanguageSr_el.php index 57c78b84..66118cc9 100644 --- a/languages/classes/LanguageSr_el.php +++ b/languages/classes/LanguageSr_el.php @@ -24,4 +24,4 @@ class LanguageSr_el extends Language { } } -?> + diff --git a/languages/classes/LanguageTr.php b/languages/classes/LanguageTr.php index 45c7f022..9a051a5c 100644 --- a/languages/classes/LanguageTr.php +++ b/languages/classes/LanguageTr.php @@ -14,4 +14,4 @@ class LanguageTr extends Language { } } -?> + diff --git a/languages/classes/LanguageTyv.php b/languages/classes/LanguageTyv.php index e912fe80..fd0a18c6 100644 --- a/languages/classes/LanguageTyv.php +++ b/languages/classes/LanguageTyv.php @@ -229,4 +229,4 @@ class LanguageTyv extends Language { return $word; } } -?> + diff --git a/languages/classes/LanguageUk.php b/languages/classes/LanguageUk.php index a16576ec..d87b7f58 100644 --- a/languages/classes/LanguageUk.php +++ b/languages/classes/LanguageUk.php @@ -85,4 +85,4 @@ class LanguageUk extends Language { } } } -?> + diff --git a/languages/classes/LanguageWa.php b/languages/classes/LanguageWa.php index aef063db..194c7720 100644 --- a/languages/classes/LanguageWa.php +++ b/languages/classes/LanguageWa.php @@ -67,4 +67,4 @@ class LanguageWa extends Language { } } -?> + diff --git a/languages/classes/LanguageZh.deps.php b/languages/classes/LanguageZh.deps.php index 1d736340..9a9dacb1 100644 --- a/languages/classes/LanguageZh.deps.php +++ b/languages/classes/LanguageZh.deps.php @@ -7,4 +7,4 @@ require_once( dirname(__FILE__).'/LanguageZh_cn.php' ); require_once( dirname(__FILE__).'/../LanguageConverter.php' ); -?> + diff --git a/languages/classes/LanguageZh.php b/languages/classes/LanguageZh.php index 9fa04985..bcdf7dd8 100644 --- a/languages/classes/LanguageZh.php +++ b/languages/classes/LanguageZh.php @@ -7,7 +7,7 @@ require_once( dirname(__FILE__).'/LanguageZh_cn.php' ); class ZhConverter extends LanguageConverter { function loadDefaultTables() { - require( "includes/ZhConversion.php" ); + require( dirname(__FILE__)."/../../includes/ZhConversion.php" ); $this->mTables = array( 'zh-cn' => new ReplacementArray( $zh2CN ), 'zh-tw' => new ReplacementArray( $zh2TW ), @@ -99,4 +99,4 @@ class LanguageZh extends LanguageZh_cn { } } -?> + diff --git a/languages/classes/LanguageZh_cn.php b/languages/classes/LanguageZh_cn.php index 1f40388c..8f54648a 100644 --- a/languages/classes/LanguageZh_cn.php +++ b/languages/classes/LanguageZh_cn.php @@ -23,4 +23,4 @@ class LanguageZh_cn extends Language { } -?> + diff --git a/languages/classes/LanguageZh_yue.php b/languages/classes/LanguageZh_yue.php index dd2d30a5..d300ea1e 100644 --- a/languages/classes/LanguageZh_yue.php +++ b/languages/classes/LanguageZh_yue.php @@ -23,4 +23,4 @@ class LanguageZh_yue extends Language { } -?> + diff --git a/languages/messages/MessagesAb.php b/languages/messages/MessagesAb.php index 7be9a316..09d5ea24 100644 --- a/languages/messages/MessagesAb.php +++ b/languages/messages/MessagesAb.php @@ -2,4 +2,4 @@ $fallback = 'ru'; -?> + diff --git a/languages/messages/MessagesAf.php b/languages/messages/MessagesAf.php index 42b0027d..b7c17324 100644 --- a/languages/messages/MessagesAf.php +++ b/languages/messages/MessagesAf.php @@ -34,363 +34,543 @@ $separatorTransformTable = array( ',' => "\xc2\xa0", '.' => ',' ); $linkTrail = "/^([a-z]+)(.*)\$/sD"; $messages = array( -# User Toggles - -"tog-underline" => "Onderstreep skakels.", -"tog-highlightbroken" => "Wys gebroke skakels so of so?).", -"tog-justify" => "Justeer paragrawe.", -"tog-hideminor" => "Moenie klein wysigings in die nuwe wysigingslys wys nie.", -"tog-usenewrc" => "Verbeterde nuwe wysigingslys (vir moderne blaaiers).", -"tog-numberheadings" => "Automatiese nommer opskrifte.", -"tog-showtoolbar" => "Show edit toolbar", -"tog-rememberpassword" => "Onthou wagwoord oor sessies.", -"tog-editwidth" => "Wysigingsboks met volle wydte.", -"tog-editondblclick" => "Wysig blaaie met dubbelkliek (JavaScript).", -"tog-watchdefault" => "Lys nuwe en gewysigde bladsye.", -"tog-minordefault" => "Merk alle wysigings automaties as klein by verstek.", -"tog-previewontop" => "Wys voorskou bo wysigingsboks.", +# User preference toggles +'tog-underline' => 'Onderstreep skakels.', +'tog-highlightbroken' => 'Wys gebroke skakels so (andersins: so?)', +'tog-justify' => 'Justeer paragrawe.', +'tog-hideminor' => 'Moenie klein wysigings in die onlangse wysigingslys wys nie.', +'tog-extendwatchlist' => 'Brei dophoulys uit om alle toepaslike wysigings te wys', +'tog-usenewrc' => 'Verbeterde onlangse wysigingslys (vir moderne blaaiers).', +'tog-numberheadings' => 'Nommer opskrifte outomaties', +'tog-showtoolbar' => 'Wys redigeergereedskap', +'tog-editondblclick' => 'Wysig blaaie met dubbelkliek (JavaScript).', +'tog-editsection' => 'Wys [wysig]-skakels vir elke afdeling', +'tog-editsectiononrightclick' => 'Wysig afdeling met regskliek op afdeling se titel (JavaScript)', +'tog-showtoc' => 'Wys inhoudsopgawe (by bladsye met meer as drie opskrifte)', +'tog-rememberpassword' => 'Onthou wagwoord oor sessies.', +'tog-editwidth' => 'Wysigingsboks met volle wydte.', +'tog-watchcreations' => 'Voeg bladsye wat ek skep by my dophoulys', +'tog-watchdefault' => 'Lys nuwe en gewysigde bladsye.', +'tog-watchmoves' => 'Voeg die bladsye wat ek skuif by my dophoulys', +'tog-watchdeletion' => 'Voeg bladsye wat ek verwyder by my dophoulys', +'tog-minordefault' => 'Merk alle wysigings automaties as klein by verstek.', +'tog-previewontop' => 'Wys voorskou bo wysigingsboks.', +'tog-previewonfirst' => 'Wys voorksou met eerste wysiging', +'tog-nocache' => 'Deaktiveer bladsykasstelsel (Engels: caching)', +'tog-enotifwatchlistpages' => 'Stuur vir my epos met bladsyveranderings', +'tog-enotifusertalkpages' => 'Stuur vir my epos as my eie besprekingsblad verander word', +'tog-enotifminoredits' => 'Stuur ook epos vir klein bladsywysigings', +'tog-enotifrevealaddr' => 'Stel my eposadres bloot in kennisgewingspos', +'tog-fancysig' => 'Doodgewone handtekening (sonder outomatiese skakel)', +'tog-externaleditor' => "Gebruik outomaties 'n eksterne redigeringsprogram", +'tog-showjumplinks' => 'Wys "spring na"-skakels vir toeganklikheid', +'tog-uselivepreview' => 'Gebruik lewendige voorskou (JavaScript) (eksperimenteel)', +'tog-forceeditsummary' => "Let my daarop as ek nie 'n opsomming van my wysiging gee nie", +'tog-watchlisthideown' => 'Versteek my wysigings in dophoulys', +'tog-watchlisthidebots' => 'Versteek robotwysigings in dophoulys', +'tog-watchlisthideminor' => 'Versteek klein wysigings van my dophoulys', +'tog-ccmeonemails' => "Stuur my 'n kopie van die e-pos wat ek aan ander stuur", +'tog-diffonly' => "Moenie 'n bladsy se inhoud onder die wysigingsverskil wys nie", + +'underline-always' => 'Altyd', +'underline-never' => 'Nooit', +'underline-default' => 'Blaaierverstek', + +'skinpreview' => '(Voorskou)', # Dates -'sunday' => 'Sondag', -'monday' => 'Maandag', -'tuesday' => 'Dinsdag', +'sunday' => 'Sondag', +'monday' => 'Maandag', +'tuesday' => 'Dinsdag', 'wednesday' => 'Woensdag', -'thursday' => 'Donderdag', -'friday' => 'Vrydag', -'saturday' => 'Saterdag', -'january' => 'Januarie', -'february' => 'Februarie', -'march' => 'Maart', -'april' => 'April', -'may_long' => 'Mei', -'june' => 'Junie', -'july' => 'Julie', -'august' => 'Augustus', +'thursday' => 'Donderdag', +'friday' => 'Vrydag', +'saturday' => 'Saterdag', +'sun' => 'So', +'mon' => 'Ma', +'tue' => 'Di', +'wed' => 'Wo', +'thu' => 'Do', +'fri' => 'Vr', +'sat' => 'Sa', +'january' => 'Januarie', +'february' => 'Februarie', +'march' => 'Maart', +'april' => 'April', +'may_long' => 'Mei', +'june' => 'Junie', +'july' => 'Julie', +'august' => 'Augustus', 'september' => 'September', -'october' => 'Oktober', -'november' => 'November', -'december' => 'Desember', -'jan' => '01', -'feb' => '02', -'mar' => '03', -'apr' => '04', -'may' => '05', -'jun' => '06', -'jul' => '07', -'aug' => '08', -'sep' => '09', -'oct' => '10', -'nov' => '11', -'dec' => '12', - - -# Bits of text used by many pages: -# -"mainpage" => "Tuisblad", -"about" => "Omtrent", -"aboutsite" => "Inligting oor {{SITENAME}}", -"aboutpage" => "{{ns:4}}:Omtrent", -"help" => "Help", -"helppage" => "{{ns:4}}:Hulp", -"bugreports" => "Foutrapporte", -"bugreportspage" => "{{ns:4}}:FoutRapporte", -"faq" => "Gewilde vrae", -"faqpage" => "{{ns:4}}:GewildeVrae", -"edithelp" => "Wysighulp", -"edithelppage" => "{{ns:4}}:Hoe_word_'n_bladsy_gewysig", -"cancel" => "Kanselleer", -"qbfind" => "Vind", -"qbbrowse" => "Snuffel", -"qbedit" => "Wysig", -"qbpageoptions" => "Bladsy opsies", -"qbpageinfo" => "Bladsy inligting", -"qbmyoptions" => "My opsies", -"mypage" => "My bladsy", -"mytalk" => "My besprekings", -"currentevents" => "Huidige gebeure", -"errorpagetitle" => "Fout", -"returnto" => "Keer terug na $1.", -"whatlinkshere" => "Bladsye wat hierheen skakel", -"help" => "Hulp", -"search" => "Soek", -"searchbutton" => "Soek", -"go" => "Wys", -'searcharticle' => "Wys", -"history" => "Ouer weergawes", -"printableversion" => "Drukbare weergawe", -"editthispage" => "Wysig hierdie bladsy", -"deletethispage" => "Skrap bladsy", -"protectthispage" => "Beskerm hierdie bladsy", -"unprotectthispage" => "Laat toe dat bladsy gewysig word", -"newpage" => "Nuwe bladsy", -"talkpage" => "Bespreek hierdie bladsy", -"articlepage" => "Lees artikel", -"userpage" => "Lees gebruikersbladsy", -"projectpage" => "Lees metabladsy", -"imagepage" => "Lees bladsy oor prent", -"viewtalkpage" => "Lees bespreking", -"otherlanguages" => "Ander tale", -"redirectedfrom" => "(Van $1 aangestuur.)", -"lastmodifiedat" => "Laaste wysiging op $2, $1.", -"viewcount" => "Hierdie bladsy is al $1 keer aangevra.", -"protectedpage" => "Beskermde bladsy", - -"nbytes" => "$1 grepe", -"ok" => "Aanvaar", #fixMe -"retrievedfrom" => "Ontsluit van \"$1\"", -"newmessageslink" => "nuwe boodskappe", +'october' => 'Oktober', +'november' => 'November', +'december' => 'Desember', +'jan' => 'Jan', +'feb' => 'Feb', +'mar' => 'Mrt', +'apr' => 'Apr', +'may' => 'Mei', +'jun' => 'Jun', +'jul' => 'Jul', +'aug' => 'Aug', +'sep' => 'Sep', +'oct' => 'Okt', +'nov' => 'Nov', +'dec' => 'Des', + +# Bits of text used by many pages +'categories' => 'Kategorieë', +'pagecategories' => '{{PLURAL:$1|Kategorie|Kategorieë}}', +'category_header' => 'Artikels in "$1"-kategorie', +'subcategories' => 'Subkategorieë', + +'about' => 'Omtrent', +'newwindow' => '(verskyn in nuwe venster)', +'cancel' => 'Kanselleer', +'qbfind' => 'Vind', +'qbbrowse' => 'Snuffel', +'qbedit' => 'Wysig', +'qbpageoptions' => 'Bladsyopsies', +'qbpageinfo' => 'Bladsyinligting', +'qbmyoptions' => 'My opsies', +'qbspecialpages' => 'Spesiale bladsye', +'moredotdotdot' => 'Meer...', +'mypage' => 'My bladsy', +'mytalk' => 'My besprekings', +'anontalk' => 'Besprekingsblad vir hierdie IP', +'navigation' => 'Navigasie', + +'errorpagetitle' => 'Fout', +'returnto' => 'Keer terug na $1.', +'tagline' => 'Vanuit {{SITENAME}}', +'help' => 'Hulp', +'search' => 'Soek', +'searchbutton' => 'Soek', +'go' => 'Wys', +'searcharticle' => 'Wys', +'history' => 'Ouer weergawes', +'history_short' => 'Geskiedenis', +'printableversion' => 'Drukbare weergawe', +'permalink' => 'Permanente skakel', +'print' => 'Druk', +'edit' => 'Wysig', +'editthispage' => 'Wysig hierdie bladsy', +'delete' => 'Skrap', +'deletethispage' => 'Skrap bladsy', +'undelete_short' => 'Herstel {{PLURAL:$1|een wysiging|$1 wysigings}}', +'protect' => 'Beskerm', +'protectthispage' => 'Beskerm hierdie bladsy', +'unprotect' => 'Verwyder beskerming', +'unprotectthispage' => 'Verwyder beskerming', +'newpage' => 'Nuwe bladsy', +'talkpage' => 'Bespreek hierdie bladsy', +'talkpagelinktext' => 'Besprekings', +'specialpage' => 'Spesiale bladsy', +'personaltools' => 'Persoonlike gereedskap', +'articlepage' => 'Lees artikel', +'talk' => 'Bespreking', +'views' => 'Aansigte', +'toolbox' => 'Gereedskap', +'userpage' => 'Lees gebruikersbladsy', +'projectpage' => 'Lees metabladsy', +'imagepage' => 'Lees bladsy oor prent', +'categorypage' => 'Bekyk kategorieblad', +'viewtalkpage' => 'Lees bespreking', +'otherlanguages' => 'Ander tale', +'redirectedfrom' => '(Aangestuur vanaf $1)', +'redirectpagesub' => 'Aanstuurblad', +'lastmodifiedat' => 'Laaste wysiging op $2, $1.', # $1 date, $2 time +'viewcount' => 'Hierdie bladsy is al $1 keer aangevra.', +'protectedpage' => 'Beskermde bladsy', +'jumpto' => 'Spring na:', +'jumptonavigation' => 'navigasie', +'jumptosearch' => 'soek', + +# All link text and link target definitions of links into project namespace that get used by other message strings, with the exception of user group pages (see grouppage) and the disambiguation template definition (see disambiguations). +'aboutsite' => 'Inligting oor {{SITENAME}}', +'aboutpage' => '{{ns:4}}:Omtrent', +'bugreports' => 'Foutrapporte', +'bugreportspage' => '{{ns:4}}:FoutRapporte', +'copyright' => 'Teks is beskikbaar onderhewig aan $1.', +'copyrightpagename' => '{{SITENAME}} kopiereg', +'copyrightpage' => '{{ns:4}}:kopiereg', +'currentevents' => 'Huidige gebeure', +'disclaimers' => 'Voorbehoud', +'edithelp' => 'Wysighulp', +'edithelppage' => '{{ns:4}}:Redigeer', +'faq' => 'Gewilde vrae', +'faqpage' => '{{ns:4}}:GewildeVrae', +'helppage' => '{{ns:4}}:Hulp', +'mainpage' => 'Tuisblad', +'policy-url' => 'Project:Policy', +'portal' => 'Gebruikersportaal', +'portal-url' => 'Project:Gebruikersportaal', +'privacy' => 'Privaatheidsbeleid', +'privacypage' => 'Project:Privaatheidsbeleid', +'sitesupport' => 'Skenkings', + +'ok' => 'Aanvaar', +'retrievedfrom' => 'Ontsluit van "$1"', +'youhavenewmessages' => 'Jy het $1 (sien $2).', +'newmessageslink' => 'nuwe boodskappe', +'newmessagesdifflink' => 'die laaste wysiging', +'editsection' => 'wysig', +'editold' => 'wysig', +'editsectionhint' => 'Wysig afdeling: $1', +'toc' => 'Inhoud', +'showtoc' => 'wys', +'hidetoc' => 'versteek', +'thisisdeleted' => 'Bekyk of herstel $1?', +'restorelink' => '{{PLURAL:$1|die een geskrapte wysiging|$1 geskrapte wysiging}}', + +# Short words for each namespace, by default used in the 'article' tab in monobook +'nstab-main' => 'Artikel', +'nstab-user' => 'Gebruikerblad', +'nstab-media' => 'Mediablad', +'nstab-special' => 'Spesiaal', +'nstab-project' => 'Projekblad', +'nstab-image' => 'Beeld', +'nstab-mediawiki' => 'Boodskap', +'nstab-template' => 'Sjabloon', +'nstab-help' => 'Hulpblad', +'nstab-category' => 'Kategorie', # Main script and global functions -# -"nosuchaction" => "Ongeldige aksie", -"nosuchactiontext" => "Onbekende aksie deur die adres gespesifeer", -"nosuchspecialpage" => "Ongeldige spesiale bladsy", -"nospecialpagetext" => "Ongeldige spesiale bladsy gespesifeer.", +'nosuchaction' => 'Ongeldige aksie', +'nosuchactiontext' => 'Onbekende aksie deur die adres gespesifeer', +'nosuchspecialpage' => 'Ongeldige spesiale bladsy', +'nospecialpagetext' => 'Ongeldige spesiale bladsy gespesifeer.', # General errors -# -"error" => "Fout", -"databaseerror" => "Databasisfout", -"dberrortext" => "Sintaksisfout in databasisnavraag. +'error' => 'Fout', +'databaseerror' => 'Databasisfout', +'dberrortext' => 'Sintaksisfout in databasisnavraag. Die laaste navraag was:
    $1
    -van funksie \"$2\". -MySQL foutboodskap \"$3: $4\".", -"noconnect" => "Kon nie met databasis op $1 konnekteer nie", -"nodb" => "Kon nie databasis $1 selekteer nie", -"readonly" => "Databasis gesluit", -"enterlockreason" => "Rede vir die sluiting, -en beraming van wanneer ontsluiting sal plaas vind", -"readonlytext" => "Die {{SITENAME}} databasis is tans gesluit vir nuwe +van funksie "$2". +MySQL foutboodskap "$3: $4".', +'noconnect' => 'Kon nie met databasis op $1 konnekteer nie', +'nodb' => 'Kon nie databasis $1 selekteer nie', +'cachederror' => "Die volgende is 'n gekaste kopie van die aangevraagde blad, en is dalk nie op datum nie.", +'readonly' => 'Databasis gesluit', +'enterlockreason' => 'Rede vir die sluiting, +en beraming van wanneer ontsluiting sal plaas vind', +'readonlytext' => 'Die {{SITENAME}} databasis is tans gesluit vir nuwe artikelwysigings, waarskynlik vir roetine databasisonderhoud, waarna dit terug sal wees na normaal. Die administreerder wat dit gesluit het se verduideliking: -

    $1", -"missingarticle" => "Die databasis het nie die teks van die veronderstelde bladsy \"$1\" gekry nie. +

    $1', +'missingarticle' => 'Die databasis het nie die teks van die veronderstelde bladsy "$1" gekry nie. Nie databasisfout nie, moontlik sagtewarefout. -Raporteer die adres asseblief aan enige administrateur.", -"internalerror" => "Interne fout", -"filecopyerror" => "Kon nie lêer van \"$1\" na \"$2\" kopieer nie.", -"filerenameerror" => "Kon nie lêernaam van \"$1\" na \"$2\" wysig nie.", -"filedeleteerror" => "Kon nie lêer \"$1\" skrap nie.", -"filenotfound" => "Kon nie lêer \"$1\" vind nie.", -"unexpected" => "Onverwagte waarde: \"$1\"=\"$2\".", -"formerror" => "Fout: kon vorm nie stuur nie", -"badarticleerror" => "Die aksie kon nie op hierdie bladsy uitgevoer word nie.", -"cannotdelete" => "Kon nie die bladsy of prent skrap nie, iemand anders het dit miskien reeds geskrap.", -"badtitle" => "Ongeldige titel", -"badtitletext" => "Die bladsytitel waarvoor gevra is, is ongeldig, leeg, of +Raporteer die adres asseblief aan enige administrateur.', +'readonly_lag' => 'Die databasis is outomaties gesluit terwyl die slaafdatabasisse sinchroniseer met die meester', +'internalerror' => 'Interne fout', +'filecopyerror' => 'Kon nie lêer van "$1" na "$2" kopieer nie.', +'filerenameerror' => 'Kon nie lêernaam van "$1" na "$2" wysig nie.', +'filedeleteerror' => 'Kon nie lêer "$1" skrap nie.', +'filenotfound' => 'Kon nie lêer "$1" vind nie.', +'unexpected' => 'Onverwagte waarde: "$1"="$2".', +'formerror' => 'Fout: kon vorm nie stuur nie', +'badarticleerror' => 'Die aksie kon nie op hierdie bladsy uitgevoer word nie.', +'cannotdelete' => 'Kon nie die bladsy of prent skrap nie, iemand anders het dit miskien reeds geskrap.', +'badtitle' => 'Ongeldige titel', +'badtitletext' => "Die bladsytitel waarvoor gevra is, is ongeldig, leeg, of 'n verkeerd geskakelde tussen-taal of tussen-wiki titel.", -"perfdisabled" => "Hierdie funksie is afgeskakel tydens spitstoegangsure vir verrigtingsredes, probeer weer tussen 02:00z en 14:00z (Universeel Gekoördineerde Tyd - UGT).", +'perfdisabled' => 'Jammer, hierdie funksie is tydelik afgeskakel omdat dit die databasis soveel verstadig dat dit onbruikbaar vir andere raak.', +'perfcached' => "ie volgende inligting is 'n gekaste kopie en mag dalk nie volledig op datum wees nie.", +'perfcachedts' => 'Die volgende data is gekas. Laaste opdatering: $1', +'viewsource' => 'Bekyk bronteks', # Login and logout pages -# -"logouttitle" => "Teken uit", -"logouttext" => "Jy is nou uitgeteken, en kan aanhou om +'logouttitle' => 'Teken uit', +'logouttext' => "Jy is nou uitgeteken, en kan aanhou om {{SITENAME}} anoniem te gebruik; of jy kan inteken as dieselfde of 'n ander gebruiker.", +'welcomecreation' => '

    Welkom, $1.

    Jou rekening is geskep; +moenie vergeet om jou persoonlike voorkeure te stel nie.', +'loginpagetitle' => 'Teken in', +'yourname' => 'Gebruikersnaam', +'yourpassword' => 'Wagwoord', +'yourpasswordagain' => 'Herhaal wagwoord', +'remembermypassword' => 'Onthou my wagwoord oor sessies.', +'loginproblem' => 'Daar was probleme met jou intekening.
    Probeer weer.', +'alreadyloggedin' => 'Gebruiker $1, jy is reeds ingeteken.
    ', +'login' => 'Teken in', +'loginprompt' => 'U blaaier moet koekies toelaat om by {{SITENAME}} aan te teken.', +'userlogin' => 'Teken in', +'logout' => 'Teken uit', +'userlogout' => 'Teken uit', +'nologin' => 'Nog nie geregistreer nie? $1.', +'nologinlink' => "Skep gerus 'n rekening", +'createaccount' => 'Skep nuwe rekening', +'gotaccount' => "Het u reeds 'n rekening? $1.", +'gotaccountlink' => 'Teken gerus aan', +'badretype' => 'Die ingetikte wagwoorde is nie dieselfde nie.', +'userexists' => "Die gebruikersnaam wat jy gebruik het, is alreeds gebruik. Kies asseblief 'n ander gebruikersnaam.", +'youremail' => 'E-pos', +'username' => 'Gebruikernaam:', +'uid' => 'Gebruiker-ID:', +'yourrealname' => 'Regte naam:', +'yourlanguage' => 'Taal:', +'yournick' => 'Bynaam (vir handtekening)', +'badsig' => 'Ongeldige handtekening; gaan HTML na.', +'email' => 'E-pos', +'loginerror' => 'Intekenfout', +'nocookieslogin' => '{{SITENAME}} gebruik koekies vir die aanteken van gebruikers, maar u blaaier laat dit nie toe nie. Skakel dit asseblief aan en probeer weer.', +'noname' => 'Ongeldige gebruikersnaam.', +'loginsuccesstitle' => 'Suksesvolle intekening', +'loginsuccess' => 'Jy is ingeteken by {{SITENAME}} as "$1".', +'nosuchuser' => 'Daar is geen gebruikersnaam "$1" nie. Maak seker dit is reg gespel, of gebruik die vorm hier onder om \'n nuwe rekening te skep.', +'wrongpassword' => 'Ongeldige wagwoord, probeer weer.', +'wrongpasswordempty' => 'Die wagwoord was leeg. Probeer asseblief weer.', +'mailmypassword' => 'E-pos nuwe wagwoord', +'passwordremindertitle' => 'Wagwoordwenk van {{SITENAME}}', +'passwordremindertext' => 'Iemand (waarskynlik jy, van IP-adres $1) +het gevra dat ons vir jou \'n nuwe {{SITENAME}} wagwoord stuur. +Die wagwoord vir gebruiker "$2" is nou "$3". +Teken asseblief in en verander jou wagwoord.', +'noemail' => 'Daar is geen e-posadres vir gebruiker "$1" nie.', +'passwordsent' => 'Nuwe wagwoord gestuur na e-posadres vir "$1". +Teken asseblief in na jy dit ontvang het.', +'eauthentsent' => "'n Bevestigingpos is gestuur na die gekose e-posadres. +Voordat ander pos na die adres gestuur word, +moet die instruksies in bogenoemde pos gevolg word om te bevestig dat die adres werklik u adres is.", +'acct_creation_throttle_hit' => 'Jammer. U het reeds $1 rekeninge geskep. U kan nie nog skep nie.', +'emailauthenticated' => 'U e-posadres is bevestig op $1.', +'emailnotauthenticated' => 'U e-poasadres is nog nie bevestig nie. Geen e-pos sal gestuur word vir die volgende funksies nie.', +'noemailprefs' => "Spesifiseer 'n eposadres vir hierdie funksies om te werk.", +'emailconfirmlink' => 'Bevestig u e-posadres', +'accountcreated' => 'Rekening geskep', +'accountcreatedtext' => 'Die rekening vir gebruiker $1 is geskep.', -"welcomecreation" => "

    Welkom, $1.

    Jou rekening is geskep; -moenie vergeet om jou persoonlike voorkeure te stel nie.", - -"loginpagetitle" => "Teken in", -"yourname" => "Jou gebruikersnaam", -"yourpassword" => "Jou wagwoord", -"yourpasswordagain" => "Tik weer jou wagwoord in", -"remembermypassword" => "Onthou my wagwoord oor sessies.", -"loginproblem" => "Daar was probleme met jou intekening.
    Probeer weer.", -"alreadyloggedin" => "Gebruiker $1, jy is reeds ingeteken.
    ", - -"login" => "Teken in", -"userlogin" => "Teken in", -"logout" => "Teken uit", -"userlogout" => "Teken uit", -"createaccount" => "Kies nuwe wagwoord", -"badretype" => "Die wagwoorde wat jy ingetik het, is nie dieselfde nie.", -"userexists" => "Die gebruikersnaam wat jy gebruik het, is alreeds gebruik. Kies asseblief 'n ander gebruikersnaam.", -"youremail" => "Jou e-pos", -"yournick" => "Jou bynaam (vir stempel)", -"loginerror" => "Intekenfout", -"noname" => "Ongeldige gebruikersnaam.", -"loginsuccesstitle" => "Suksesvolle intekening", -"loginsuccess" => "Jy is ingeteken by {{SITENAME}} as \"$1\".", -"nosuchuser" => "Daar is geen \"$1\" gebruikersnaam nie. -Maak seker dit is reg gespel, of gebruik die vorm hier onder om 'n nuwe rekening te skep.", -"wrongpassword" => "Ongeldige wagwoord, probeer weer.", -"mailmypassword" => "Stuur my wagwword na my e-pos adres.", -"passwordremindertitle" => "Wagwoordwenk van {{SITENAME}}", -"passwordremindertext" => "Iemand (waarskynlik jy, van IP-adres $1) -het gevra dat ons vir jou 'n nuwe {{SITENAME}} wagwoord stuur. -Die wagwoord vir gebruiker \"$2\" is nou \"$3\". -Teken asseblief in en verander jou wagwoord.", -"noemail" => "Daar is geen e-pos adres vir gebruiker \"$1\" nie.", -"passwordsent" => "Nuwe wagwoord gestuur na e-posadres vir \"$1\". -Teken asseblief in na jy dit ontvang het.", +# Edit page toolbar +'bold_sample' => 'Vet teks', +'bold_tip' => 'Vetdruk', +'italic_sample' => 'Skuins teks', +'italic_tip' => 'Skuinsdruk', +'link_sample' => 'Skakelnaam', +'link_tip' => 'Interne skakel', +'extlink_sample' => 'http://www.voorbeeld.org skakel se titel', +'extlink_tip' => 'Eksterne skakel (onthou http:// vooraan)', +'headline_sample' => 'Opskrif', +'headline_tip' => 'Vlak 2-opskrif', +'math_sample' => 'Plaas formule hier', +'math_tip' => 'Wiskundige formule (LaTeX)', +'nowiki_sample' => 'Plaas ongeformatteerde teks hier', +'nowiki_tip' => 'Ignoreer wiki-formattering', +'image_sample' => 'Voorbeeld.jpg', +'image_tip' => 'Beeld/prentjie/diagram', +'media_sample' => 'Voorbeeld.ogg', +'media_tip' => 'Skakel na ander tipe medialêer', +'sig_tip' => 'Handtekening met datum', +'hr_tip' => 'Horisontale streep (selde nodig)', # Edit pages -# -"summary" => "Opsomming", -"minoredit" => "Klein wysiging", -"watchthis" => "Hou bladsy dop", -"savearticle" => "Stoor bladsy", -"preview" => "Voorskou", -"showpreview" => "Wys voorskou", -"blockedtitle" => "Gebruiker is geblokkeer", -"blockedtext" => "Jou gebruikersnaam of IP-adres is deur $1 geblokkeer: +'summary' => 'Opsomming', +'subject' => 'Onderwerp/opskrif', +'minoredit' => 'Klein wysiging', +'watchthis' => 'Hou bladsy dop', +'savearticle' => 'Stoor bladsy', +'preview' => 'Voorskou', +'showpreview' => 'Wys voorskou', +'showdiff' => 'Wys veranderings', +'anoneditwarning' => "'''Waarskuwing:''' Aangesien u nie aangeteken is nie, sal u IP-adres in dié blad se wysigingsgeskiedenis gestoor word.", +'missingsummary' => "'''Onthou:''' Geen opsomming van die wysiging is verskaf nie. As \"Stoor\" weer geklik word, word die wysiging sonder opsomming gestoor.", +'missingcommenttext' => 'Tik die opsomming onder.', +'summary-preview' => 'Summary preview', +'blockedtitle' => 'Gebruiker is geblokkeer', +'blockedtext' => "Jou gebruikersnaam of IP-adres is deur $1 geblokkeer:
    ''$2''

    Jy mag $1 of een van die ander [[{{MediaWiki:grouppage-sysop}}|administreerders]] kontak om dit te bespreek.", -"newarticle" => "(Nuut)", -"newarticletext" => -"Die bladsy waarna geskakel is, bestaan nie. +'confirmedittitle' => 'E-pos-bevestiging nodig om te redigeer', +'confirmedittext' => 'U moet u e-posadres bevestig voor u bladsye wysig. Verstel en bevestig asseblief u e-posadres by u [[Spesiaal:Preferences|voorkeure]].', +'accmailtitle' => 'Wagwoord gestuur.', +'accmailtext' => "Die wagwoord van '$1' is gestuur aan $2.", +'newarticle' => '(Nuut)', +'newarticletext' => "Die bladsy waarna geskakel is, bestaan nie. Om 'n nuwe bladsy te skep, tik in die invoerboks hier onder. Lees die [[{{MediaWiki:helppage}}|hulp bladsy]] vir meer inligting. Indien jy per ongeluk hier is, gebruik jou blaaier se '''terug''' knop.", -"anontalkpagetext" => "---- ''Dit is die besprekingsbladsy vir 'n anonieme gebruiker wat nie 'n rekening geskep het nie. Ons moet dus hul IP-adres gebruik om hulle te identifiseer. So 'n IP-adres kan deur verskeie gebruikers gedeel word. Indien jy 'n anonieme gebruiker is wat voel dat oneerbiedige komentaar aan jou gerig is, [[Special:Userlogin|skep 'n rekening of teken in]] om verwarring te voorkom met ander anonieme gebruikers.''", -"noarticletext" => "(Daar is tans geen inligting vir hierdie artikel nie.)", -"updated" => "(Gewysig)", -"note" => "Nota:", -"previewnote" => "Onthou dat dit slegs 'n voorskou is en nog nie gestoor is nie!", -"previewconflict" => "Hierdie voorskou reflekteer die teks in die boonste invoerboks soos dit sal lyk as jy dit stoor.", -"editing" => "Besig om $1 te wysig", -'editinguser' => "Besig om $1 te wysig", -"editconflict" => "Wysigingskonflik: $1", -"explainconflict" => "Iemand anders het hierdie bladsy gewysig sedert jy dit begin verander het. +'anontalkpagetext' => "----''Hierdie is die besprekingsblad vir 'n anonieme gebruiker wat nog nie 'n rekening geskep het nie of wat dit nie gebruik nie. Daarom moet ons sy/haar numeriese [[IP-adres]] gebruik vir identifikasie. Só 'n adres kan deur verskeie gebruikers gedeel word. Indien jy 'n anonieme gebruiker is wat voel dat ontoepaslike kommentaar teen jou gerig is, [[Spesiaal:Userlogin|skep 'n rekening of teken in]] om verwarring met ander anonieme gebruikers te voorkom.''", +'noarticletext' => '(Daar is tans geen inligting vir hierdie artikel nie.)', +'clearyourcache' => "'''Let wel''': Na die voorkeure gestoor is, moet u blaaier se kasgeheue verfris word om die veranderinge te sien: '''Mozilla:''' klik ''Reload'' (of ''Ctrl-R''), '''IE / Opera:''' ''Ctrl-F5'', '''Safari:''' ''Cmd-R'', '''Konqueror''' ''Ctrl-R''.", +'usercssjsyoucanpreview' => 'Wenk: Gebruik die "Wys voorskou"-knoppie om u nuwe CSS/JS te toets voor u stoor.', +'usercsspreview' => "'''Onthou hierdie is slegs 'n voorskou van u gebruiker-CSS, dit is nog nie gestoor nie.'''", +'userjspreview' => "'''Onthou hierdie is slegs 'n toets/voorskou van u gebruiker-JavaScript, dit is nog nie gestoor nie.'''", +'updated' => '(Gewysig)', +'note' => 'Nota:', +'previewnote' => "Onthou dat hierdie slegs 'n voorskou is en nog nie gestoor is nie!", +'previewconflict' => 'Hierdie voorskou vertoon die teks in die boonste teksarea soos dit sou lyk indien jy die bladsy stoor.', +'session_fail_preview' => 'Jammer! Weens verlies aan sessie-inligting is die wysiging nie verwerk nie. +Probeer asseblief weer. As dit steeds nie werk nie, probeer om af en weer aan te teken.', +'editing' => 'Besig om $1 te wysig', +'editinguser' => 'Besig om $1 te wysig', +'editingsection' => 'Besig om $1 (onderafdeling) te wysig', +'editingcomment' => 'Besig om $1 (kommentaar) te wysig', +'editconflict' => 'Wysigingskonflik: $1', +'explainconflict' => 'Iemand anders het hierdie bladsy gewysig sedert jy dit begin verander het. Die boonste invoerboks het die teks wat tans bestaan. Jou wysigings word in die onderste invoerboks gewys. Jy sal jou wysigings moet saamsmelt met die huidige teks. -Slegs die teks in die boonste invoerboks sal gestoor word wanneer jy \"Stoor bladsy\" druk.
    ", -"yourtext" => "Jou teks", -"storedversion" => "Gestoorde weergawe", -"editingold" => "Waarskuwing: jy is besig om 'n ou weergawe van hierdie bladsy te wysig. -As jy dit stoor, sal enige wysigings sedert hierdie wysiging verloor word.", -"yourdiff" => "Wysigings", -/*"copyrightwarning" => "Alle bydraes aan {{SITENAME}} word beskou as beskikbaar gestel onder -die ''GNU Free Documentation License'' (lees $1 vir meer inligting). -As jy nie wil hê dat jou werk ongemagtig gewysig of versprei mag word nie, moet jy dit nie hier indien nie.
    -Jy belowe ons ook dat jy dit self geskryf het, of verkry het van 'n bron wat toelaat dat dit hier mag wees.
    -Moenie werk beskerm deur kopiereg sonder toestemming indien nie!",*/ -"longpagewarning" => "Waarskuwing: hierdie bladsy is $1 kilogrepe lank; sekere blaaiers -kan probleme hê met die wysiging va blaaie langer as 32 kilogrepe. Breek asseblief die bladsy op in kleiner dele.", +Slegs die teks in die boonste invoerboks sal gestoor word wanneer jy "Stoor bladsy" druk.
    ', +'yourtext' => 'Jou teks', +'storedversion' => 'Gestoorde weergawe', +'editingold' => "WAARSKUWING: Jy is besig om 'n ouer weergawe van hierdie bladsy te wysig. +As jy dit stoor, sal enige wysigings sedert hierdie een weer uitgewis word.", +'yourdiff' => 'Wysigings', +'longpagewarning' => 'WAARSKUWING: Hierdie bladsy is $1 kG groot. +Probeer asseblief die bladsy verkort en die detail na subartikels skuif sodat dit nie 32 kG oorskry nie.', +'readonlywarning' => "WAARSKUWING: Die databasis is gesluit vir onderhoud. Dus sal u nie nou u wysigings kan stoor nie. Dalk wil u die teks plak in 'n lêer en stoor vir later. ", +'protectedpagewarning' => 'WAARSKUWING: Hierdie blad is beskerm, en slegs administrateurs kan die inhoud verander.', +'semiprotectedpagewarning' => "'''Let wel:''' Hierdie artikel is beskerm sodat slegs ingetekende gebruikers dit kan wysig.", +'templatesused' => 'Sjablone in gebruik op hierdie blad:', # History pages -# -"revhistory" => "Wysigingsgeskiedenis", -"nohistory" => "Daar is geen wysigingsgeskiedenis vir hierdie bladsy nie.", -"revnotfound" => "Wysiging nie gevind nie.", -"revnotfoundtext" => "Die ou wysiging waarvoor jy gevra het, kon nie gevind word nie. Maak asseblief seker dat die adres wat jy gebruik -het om toegang te kry tot hierdie bladsy, reg is.", -"loadhist" => "Besig om bladsy wysigingsgeskiedenis te laai.", -"currentrev" => "Huidige wysiging", -"revisionasof" => "Wysiging soos op $1", -"cur" => "huidige", -"next" => "volgende", -"last" => "vorige", -"orig" => "oorspronklike", -"histlegend" => "Byskrif: (huidige) = verskil van huidige weergawe, -(vorige) = verskil van vorige weergawe, M = klein wysiging", +'revhistory' => 'Wysigingsgeskiedenis', +'nohistory' => 'Daar is geen wysigingsgeskiedenis vir hierdie bladsy nie.', +'revnotfound' => 'Weergawe nie gevind nie', +'revnotfoundtext' => 'Die ou weergawe wat jy aangevra het kon nie gevind word nie. Gaan asseblief die URL na wat jy gebruik het.', +'loadhist' => 'Besig om bladsy wysigingsgeskiedenis te laai.', +'currentrev' => 'Huidige wysiging', +'revisionasof' => 'Wysiging soos op $1', +'previousrevision' => '← Ouer weergawe', +'nextrevision' => 'Nuwer weergawe →', +'currentrevisionlink' => 'bekyk huidige weergawe', +'cur' => 'huidige', +'next' => 'volgende', +'last' => 'vorige', +'orig' => 'oorspronklike', +'histlegend' => 'Byskrif: (huidige) = verskil van huidige weergawe, +(vorige) = verskil van vorige weergawe, M = klein wysiging', +'histfirst' => 'Oudste', +'histlast' => 'Nuutste', # Diffs -# -"difference" => "(Verksil tussen weergawes)", -"loadingrev" => "Besig om weergawe van verskil te laai.", -"lineno" => "Lyn $1:", -"editcurrent" => "Wysig die huidige weergawe van hierdie bladsy.", +'difference' => '(Verskil tussen weergawes)', +'loadingrev' => 'Besig om weergawe van verskil te laai.', +'lineno' => 'Lyn $1:', +'editcurrent' => 'Wysig die huidige weergawe van hierdie bladsy.', +'selectnewerversionfordiff' => "Kies 'n nuwer weergawe vir vergelyking", +'selectolderversionfordiff' => "Kies 'n ouer weergawe vir vergelyking", +'compareselectedversions' => 'Vergelyk gekose weergawes', # Search results -# -"searchresults" => "soekresultate", -"searchresulttext" => "Vir meer inligting oor {{SITENAME}} soekresultate, lees [[{{MediaWiki:helppage}}|{{int:help}}]].", -"searchsubtitle" => "Vir navraag \"[[:$1]]\"", -"searchsubtitleinvalid" => "Vir navraag \"$1\"", -"badquery" => "Verkeerd gestelde navraag", -"badquerytext" => "Ons kon nie jou naavraag prosesseer nie. -Dit is miskien omdat jy gesoek het vir iets wat minder as drie letters bevat. Jy het miskien die navraag verkeerd ingetik.", -"matchtotals" => "Die navraag \"$1\" pas $2 artikeltitels -en teks in $3 artikels.", -"noexactmatch" => "Geen bladsy met hierdie presiese titel bestaan nie, probeer 'n volteksnavraag.", -"titlematches" => "Artikeltitel resultate", -"notitlematches" => "Geen artikeltitel resultate nie", -"textmatches" => "Artikelteks resultate", -"notextmatches" => "Geen artikelteks resultate nie", -"prevn" => "vorige $1", -"nextn" => "volgende $1", -"viewprevnext" => "Kyk na ($1) ($2) ($3).", -"showingresults" => "Onder $1 resultate, beginende met #$2.", -"nonefound" => "Nota: onsuksesvolle navrae word gewoonlik veroorsaak deur 'n soektog met algemene +'searchresults' => 'soekresultate', +'searchresulttext' => 'Vir meer inligting oor {{SITENAME}} soekresultate, lees [[{{MediaWiki:helppage}}|{{int:help}}]].', +'searchsubtitle' => 'Vir navraag "[[:$1]]"', +'searchsubtitleinvalid' => 'Vir navraag "$1"', +'badquery' => 'Verkeerd gestelde navraag', +'badquerytext' => 'Die navraag kon nie verwerk word nie. Dit kan wees dat die soektog minder as drie letters bevat het, of dat die navraag verkeerd ingetik is.', +'matchtotals' => 'Die navraag "$1" pas $2 artikeltitels +en teks in $3 artikels.', +'noexactmatch' => "Geen bladsy met hierdie presiese titel bestaan nie, probeer 'n volteksnavraag.", +'titlematches' => 'Artikeltitel resultate', +'notitlematches' => 'Geen artikeltitel resultate nie', +'textmatches' => 'Artikelteks resultate', +'notextmatches' => 'Geen artikelteks resultate nie', +'prevn' => 'vorige $1', +'nextn' => 'volgende $1', +'viewprevnext' => 'Kyk na ($1) ($2) ($3).', +'showingresults' => 'Hier volg $1 resultate vanaf #$2.', +'nonefound' => "Nota: onsuksesvolle navrae word gewoonlik veroorsaak deur 'n soektog met algemene woorde wat nie geindekseer word nie, of spesifisering van meer as een woord (slegs blaaie wat alle navraagwoorde bevat, word gewys).", -"powersearch" => "Soek", -"powersearchtext" => " +'powersearch' => 'Soek', +'powersearchtext' => ' Search in namespaces :
    $1
    -$2 List redirects Search for $3 $9", #fixMe - +$2 List redirects Search for $3 $9', +'searchdisabled' => '{{SITENAME}} se soekfunksie is tans afgeskakel ter wille van werkverrigting. Gebruik gerus intussen Google of Yahoo! Let daarop dat hulle indekse van die {{SITENAME}}-inhoud verouderd mag wees.', +'blanknamespace' => '(Hoof)', # Preferences page -# -"preferences" => "Voorkeure", -"prefsnologin" => "Nie ingeteken nie", -"prefsnologintext" => "Jy moet [[Special:Userlogin|ingeteken wees]] -om voorkeure te spesifiseer.", -"prefsreset" => "Voorkeure is herstel.", -"qbsettings" => "Snelbalkvoorkeure", #fixMe Quickbar settings -'qbsettings-none' => 'Geen.', -'qbsettings-fixedleft' => 'Links vas.', -'qbsettings-fixedright' => 'Regs vas.', -'qbsettings-floatingleft' => 'Dryf links.', -'qbsettings-floatingright' => 'Dryf regs.', -"changepassword" => "Verander wagwoord", -"skin" => "Omslag", -"math" => "Verbeeld wiskunde", -"math_failure" => "Kon nie verbeeld nie", -"math_unknown_error" => "onbekende fout", -"math_unknown_function" => "onbekende funksie", -"math_lexing_error" => "leksikale fout", -"math_syntax_error" => "sintaksfout", -"saveprefs" => "Stoor voorkeure", -"resetprefs" => "Herstel voorkeure", -"oldpassword" => "Ou wagwoord", -"newpassword" => "Nuwe wagwoord", -"retypenew" => "Tik nuwe wagwoord weer in", -"textboxsize" => "Grootte van invoerboks", -"rows" => "Rye", -"columns" => "Kolomme", -"searchresultshead" => "Soekresultaat voorkeure", -"resultsperpage" => "Aantal resultate om te wys", -"contextlines" => "Aantal lyne per resultaat", -"contextchars" => "Karakters konteks per lyn", -"stubthreshold" => "Drempel vir verkorte artikels", -"recentchangescount" => "Aantal titels in onlangse wysigings", -"savedprefs" => "Jou voorkeure is gestoor.", -"timezonetext" => "Aantal ure wat plaaslike tyd verskil van UGT.", -"localtime" => "Plaaslike tyd", -"timezoneoffset" => "Teenrekening", +'preferences' => 'Voorkeure', +'mypreferences' => 'My voorkeure', +'prefsnologin' => 'Nie ingeteken nie', +'prefsnologintext' => 'Jy moet [[Special:Userlogin|ingeteken wees]] +om voorkeure te spesifiseer.', +'prefsreset' => 'Voorkeure is herstel.', +'qbsettings' => 'Snelbalkvoorkeure', +'qbsettings-none' => 'Geen.', +'qbsettings-fixedleft' => 'Links vas.', +'qbsettings-fixedright' => 'Regs vas.', +'qbsettings-floatingleft' => 'Dryf links.', +'qbsettings-floatingright' => 'Dryf regs.', +'changepassword' => 'Verander wagwoord', +'skin' => 'Omslag', +'math' => 'Wiskunde', +'dateformat' => 'Datumformaat', +'datedefault' => 'Geen voorkeur', +'datetime' => 'Datum en tyd', +'math_failure' => 'Kon nie verbeeld nie', +'math_unknown_error' => 'onbekende fout', +'math_unknown_function' => 'onbekende funksie', +'math_lexing_error' => 'leksikale fout', +'math_syntax_error' => 'sintaksfout', +'prefs-personal' => 'Gebruikersdata', +'prefs-rc' => 'Onlangse wysigings', +'prefs-watchlist' => 'Dophoulys', +'prefs-watchlist-days' => 'Aantal dae om in dophoulys te wys:', +'prefs-watchlist-edits' => 'Aantal wysigings om in uitgebreide dophoulys te wys:', +'prefs-misc' => 'Allerlei', +'saveprefs' => 'Stoor voorkeure', +'resetprefs' => 'Herstel voorkeure', +'oldpassword' => 'Ou wagwoord', +'newpassword' => 'Nuwe wagwoord', +'retypenew' => 'Tik nuwe wagwoord weer in', +'textboxsize' => 'Wysiging', +'rows' => 'Rye', +'columns' => 'Kolomme', +'searchresultshead' => 'Soekresultate', +'resultsperpage' => 'Aantal resultate om te wys', +'contextlines' => 'Aantal lyne per resultaat', +'contextchars' => 'Karakters konteks per lyn', +'recentchangesdays' => 'Aantal dae wat in onlangse wysigings vertoon word:', +'recentchangescount' => 'Aantal titels in onlangse wysigings', +'savedprefs' => 'Jou voorkeure is gestoor.', +'timezonelegend' => 'Tydsone', +'timezonetext' => 'Aantal ure waarmee plaaslike tyd van UTC verskil.', +'localtime' => 'Plaaslike tyd', +'timezoneoffset' => 'Verplasing¹', +'servertime' => 'Tyd op die bediener is nou', +'guesstimezone' => 'Vul in vanaf webblaaier', +'allowemail' => 'Laat e-pos van ander toe', +'defaultns' => 'Verstek naamruimtes vir soektog:', +'default' => 'verstek', +'files' => 'Lêers', # Recent changes -# -"changes" => "wysigings", -"recentchanges" => "Onlangse wysigings", -"rcnote" => "Hier onder is die laaste $1 wysigings gedurende die laaste $2 dae.", -"rcnotefrom" => "Hier onder is die wysigings sedert $2 (tot by $1 word gewys).", -"rclistfrom" => "Wys nuwe wysigings en begin by $1", -"rclinks" => "Wys die laaste $1 wysigings in die laaste $2 dae.", -"diff" => "verskil", -"hist" => "geskiedenis", -"hide" => "vat weg", -"show" => "wys", -"minoreditletter" => "K", -"newpageletter" => "N", +'nchanges' => '$1 {{PLURAL:$1|wysiging|wysigings}}', +'recentchanges' => 'Onlangse wysigings', +'rcnote' => 'Hier volg die laaste $1 wysigings gedurende die afgelope $2 dae.', +'rcnotefrom' => 'Hier onder is die wysigings sedert $2 (tot by $1 word gewys).', +'rclistfrom' => 'Vertoon wysigings vanaf $1', +'rcshowhideminor' => '$1 klein wysigings', +'rcshowhidebots' => '$1 robotte', +'rcshowhideliu' => '$1 aangetekende gebruikers', +'rcshowhideanons' => '$1 anonieme gebruikers', +'rcshowhidepatr' => '$1 gepatrolleerde wysigings', +'rcshowhidemine' => '$1 my wysigings', +'rclinks' => 'Vertoon die laaste $1 wysigings in die afgelope $2 dae
    $3', +'diff' => 'verskil', +'hist' => 'geskiedenis', +'hide' => 'versteek', +'show' => 'wys', +'minoreditletter' => 'k', +'newpageletter' => 'N', + +# Recent changes linked +'recentchangeslinked' => 'Verwante veranderings', # Upload -# -"upload" => "Laai lêer", -"uploadbtn" => "Laai lêer", -"reupload" => "Herlaai", -"reuploaddesc" => "Keer terug na die laaivorm.", -"uploadnologin" => "Nie ingeteken nie", -"uploadnologintext" => "Teken eers in [[Special:Userlogin|logged in]] -om lêers te laai.", -"uploaderror" => "Laaifout", -"uploadtext" => "'''STOP!''' Voor jy hier laai, lees en volg {{SITENAME}} se +'upload' => 'Laai lêer', +'uploadbtn' => 'Laai lêer', +'reupload' => 'Herlaai', +'reuploaddesc' => 'Keer terug na die laaivorm.', +'uploadnologin' => 'Nie ingeteken nie', +'uploadnologintext' => 'Teken eers in [[Special:Userlogin|logged in]] +om lêers te laai.', +'uploaderror' => 'Laaifout', +'uploadtext' => "'''STOP!''' Voor jy hier laai, lees en volg {{SITENAME}} se [[{{MediaWiki:copyrightpage}}|beleid oor prentgebruik]]. Om prente wat voorheen gelaai is te sien of te soek, gaan na die @@ -412,286 +592,446 @@ Om die prent in 'n artikel te gebruik, gebruik 'n skakel met die formaat '''[[{{ns:media}}:file.ogg]]''' vir klanklêers. Let asseblief op dat, soos met {{SITENAME}} bladsye, mag ander jou gelaaide lêers redigeer as hulle dink dit dien die ensiklopedie, en jy kan verhoed word om lêers te laai as jy die stelsel misbruik.", -"uploadlog" => "laailog", -"uploadlogpage" => "laai_log", -"uploadlogpagetext" => "Hier onder is 'n lys van die mees onlangse lêers wat gelaai is. -Alle tye is bedienertyd (UGT). -

      -
    ", -"filename" => "Lêernaam", -"filedesc" => "Opsomming", -"copyrightpage" => "{{ns:4}}:kopiereg", -"copyrightpagename" => "{{SITENAME}} kopiereg", -"uploadedfiles" => "Gelaaide lêers", -"minlength" => "Prentname moet ten minste drie letters lank wees.", -"badfilename" => "Prentnaam is verander na \"$1\".", -"badfiletype" => "\".$1\" is nie 'n aanbevole lêerformaat vir prente nie.", -"successfulupload" => "Laai suksesvol", -"fileuploaded" => "Lêer \"$1\" suksesvol gelaai. -Volg asseblief hierdie skakel: ($2) na die beskrywingsbladsy en vul inligting in oor die die lêer, soos waar dit vandaan kom, wie het dit geskep en wanneer, en enige iets anders wat jy daarvan af weet.", -"uploadwarning" => "Laaiwaarskuwing", -"savefile" => "Stoor lêer", -"uploadedimage" => "Het \"[[$1]]\" gelaai", +'uploadlog' => 'laailog', +'uploadlogpage' => 'laai_log', +'uploadlogpagetext' => "Hier volg 'n lys van die mees onlangse lêers wat gelaai is.", +'filename' => 'Lêernaam', +'filedesc' => 'Opsomming', +'fileuploadsummary' => 'Opsomming:', +'filestatus' => 'Outeursregsituasie', +'filesource' => 'Bron', +'uploadedfiles' => 'Gelaaide lêers', +'ignorewarnings' => 'Ignoreer enige waarskuwings', +'minlength1' => 'Prentname moet ten minste een letter lank wees.', +'illegalfilename' => 'Die lêernaam "$1" bevat karakters wat nie toegelaat word in bladsytitels nie. Verander asseblief die naam en probeer die lêer weer laai.', +'badfilename' => 'Prentnaam is verander na "$1".', +'largefileserver' => 'Hierdie lêer is groter as wat die bediener se opstelling toelaat.', +'emptyfile' => "Die lêer wat jy probeer oplaai het blyk leeg te wees. Dit mag wees omdat jy 'n tikfout in die lêernaam gemaak het. Gaan asseblief na en probeer weer.", +'successfulupload' => 'Laai suksesvol', +'uploadwarning' => 'Laaiwaarskuwing', +'savefile' => 'Stoor lêer', +'uploadedimage' => 'Het "[[$1]]" gelaai', +'uploadcorrupt' => "Die lêer is foutief of is van 'n verkeerde tipe. Gaan asseblief die lêer na en laai weer op.", +'sourcefilename' => 'Bronlêernaam', +'destfilename' => 'Teikenlêernaam', +'watchthisupload' => 'Hou hierdie bladsy dop', + +'license' => 'Lisensiëring', # Image list -# -"imagelist" => "Prentelys", -"imagelisttext" => "Hier onder is a lys van $1 prente gesorteer $2.", -"getimagelist" => "Besig om prentelys te haal", -"ilsubmit" => "Soek", -"showlast" => "Wys laaste $1 prente gesorteer $2.", -"byname" => "volgens naam", -"bydate" => "volgens datum", -"bysize" => "volgens grootte", -"imgdelete" => "skrap", -"imgdesc" => "beskrywing", -"imglegend" => "Legende: (beskrywing) = wys/verander prent se beskrywing.", -"imghistory" => "Prentgeskiedenis", -"revertimg" => "gaan terug", -"deleteimg" => "skrap", -"deleteimgcompletely" => "skrap", -"imghistlegend" => "Legende: (huidig) = dit is die huidige prent, (skrap) = skrap hierdie ou weergawe, (gaan terug) = gaan terug na hierdie ou weergawe. -
    Kliek die datum om die prent te sien wat op daardie datum gelaai is.", -"imagelinks" => "Prentskakels", -"linkstoimage" => "Die volgende bladsye gebruik hierdie prent:", -"nolinkstoimage" => "Daar is geen bladsye wat hierdie prent gebruik nie.", +'imagelist' => 'Prentelys', +'imagelisttext' => 'Hier onder is a lys van $1 prente gesorteer $2.', +'getimagelist' => 'Besig om prentelys te haal', +'ilsubmit' => 'Soek', +'showlast' => 'Wys laaste $1 prente gesorteer $2.', +'byname' => 'volgens naam', +'bydate' => 'volgens datum', +'bysize' => 'volgens grootte', +'imgdelete' => 'skrap', +'imgdesc' => 'beskrywing', +'imglegend' => 'Sleutel: (beskrywing) = wys/verander prent se beskrywing.', +'imghistory' => 'Prentgeskiedenis', +'revertimg' => 'gaan terug', +'deleteimg' => 'skrap', +'deleteimgcompletely' => 'skrap', +'imghistlegend' => 'Sleutel: (huidige) = dit is die huidige weergawe, (skrap) = skrap hierdie weergawe, (gaan terug) = gaan terug na hierdie ou weergawe. +
    Kliek die datum om die weergawe te sien wat op daardie datum gelaai is.', +'imagelinks' => 'Prentskakels', +'linkstoimage' => 'Die volgende bladsye gebruik hierdie prent:', +'nolinkstoimage' => 'Daar is geen bladsye wat hierdie prent gebruik nie.', +'noimage' => "Geen lêer met so 'n naam bestaan nie; $1 gerus.", +'noimage-linktext' => 'laai dit', + +# MIME search +'mimesearch' => 'MIME-soek', + +# List redirects +'listredirects' => 'Lys aansture', + +# Unused templates +'unusedtemplatestext' => "Hierdie blad lys alle bladsye in die sjabloonnaamruimte wat nêrens in 'n ander blad ingesluit word nie. Onthou om ook ander skakels na die sjablone na te gaan voor verwydering.", +'unusedtemplateswlh' => 'ander skakels', # Statistics -# -"statistics" => "Statistiek", -"sitestats" => "Werfstatistiek", -"userstats" => "Gebruikerstatistiek", -"sitestatstext" => "Daar is 'n totaal van $1 bladsye in die databasis. -Dit sluit \"bespreek\" bladsye in, bladsye oor {{SITENAME}}, minimale \"verkorte\" +'statistics' => 'Statistiek', +'sitestats' => 'Werfstatistiek', +'userstats' => 'Gebruikerstatistiek', +'sitestatstext' => 'Daar is \'n totaal van $1 bladsye in die databasis. +Dit sluit "bespreek" bladsye in, bladsye oor {{SITENAME}}, minimale "verkorte" bladsye, wegwysbladsye, en ander wat waarskynlik nie as artikels kwalifiseer nie. Uitsluitend bogenoemde, is daar $2 bladsye wat waarskynlik ware artikels is.

    Bladsye is al $3 kere aangevra, en $4 keer verander sedert die sagteware opgegradeer is (July 20, 2002). -Dit werk uit op gemiddeld $5 veranderings per bladsy, en bladsye word $6 keer per verandering aangevra.", -"userstatstext" => "Daar is $1 geregistreerde gebruikers. -$2 van hulle is administrateurs (sien $3).", +Dit werk uit op gemiddeld $5 veranderings per bladsy, en bladsye word $6 keer per verandering aangevra.', +'userstatstext' => 'Daar is $1 geregistreerde gebruikers. +$2 van hulle is administrateurs (sien $3).', + +'disambiguations' => 'Bladsye wat onduidelikhede opklaar', +'disambiguationspage' => '{{ns:4}}:Links_to_disambiguating_pages', -# Maintenance Page -# -"disambiguations" => "Bladsye wat onduidelikhede opklaar", -"disambiguationspage" => "{{ns:4}}:Links_to_disambiguating_pages", -"disambiguationstext" => "Die volgende artikels skakel na 'n bladsy wat onduidelikhede opklaar. Hulle behoort eerder na die relevante onderwerp te skakel.
    'n Bladsy word gesien as een wat onduidelikhede opklaar as $1 daarna toe skakel.
    Skakels van ander naamkontekste is nie hier gelys nie.", -"doubleredirects" => "Dubbele aansture", -"doubleredirectstext" => "Let op: Hierdie lys bevat moontlik false positiewe. Dit beteken gewoonlik dat daar nog teks met skakels onder die eerste #REDIRECT is.
    \nElke ry bevat skakels na die eerste en die tweede aanstuur, asook die eerste reël van van die tweede aanstuurteks, wat gewoonlik die \"regte\" teikenbladsy gee waarna die eerste aanstuur behoort te wys.", -"brokenredirects" => "Stukkende aansture", -"brokenredirectstext" => "Die volgende aansture skakel na 'n bladsy wat nie bestaan nie.", +'doubleredirects' => 'Dubbele aansture', +'doubleredirectstext' => 'Let op: Hierdie lys bevat moontlik false positiewe. Dit beteken gewoonlik dat daar nog teks met skakels onder die eerste #REDIRECT is.
    +Elke ry bevat skakels na die eerste en die tweede aanstuur, asook die eerste reël van van die tweede aanstuurteks, wat gewoonlik die "regte" teikenbladsy gee waarna die eerste aanstuur behoort te wys.', +'brokenredirects' => 'Stukkende aansture', +'brokenredirectstext' => "Die volgende aansture skakel na 'n bladsy wat nie bestaan nie.", # Miscellaneous special pages -# -"lonelypages" => "Weesbladsye", -"unusedimages" => "Ongebruikte prente", -"popularpages" => "Populêre bladsye", -"nviews" => "$1 keer aangevra", -"wantedpages" => "Gesogte bladsye", -"nlinks" => "$1 skakels", -"allpages" => "Alle bladsye", -"randompage" => "Lukrake bladsy", -"shortpages" => "Kort bladsye", -"longpages" => "Lang bladsye", -"listusers" => "Gebruikerslys", -"specialpages" => "Spesiale bladsye", -"spheading" => "Spesiale bladsye", -"recentchangeslinked" => "Verwante veranderings", -"rclsub" => "(na bladsye waarna \"$1\" skakel)", -"newpages" => "Nuwe bladsye", -"movethispage" => "Skuif hierdie bladsy", -"unusedimagestext" => "

    Let asseblief op dat ander webwerwe, soos die internasionale {{SITENAME}}s, dalk met 'n direkte URL na 'n prent skakel, so die prent sal dus hier verskyn al word dit aktief gebruik.", // TODO: grammar -"booksources" => "Boekbronne", -"booksourcetext" => "Hier onder is 'n lys van skakels na ander werwe wat nuwe en tweede handse boeke verkoop, en wat dalk ook verdere inligting het oor boeke waarna jy soek. -{{SITENAME}} is nie geaffilieer aan enige van hierdie besighede nie en die lys moet nie as 'n aanbeveling gesien word nie.", - -# Email this user -# -"mailnologin" => "Geen verstuuradres", -"mailnologintext" => "Jy moet [[Special:Userlogin|ingeteken]] -wees en 'n geldige e-posadres in jou [[Special:Preferences|voorkeure]] -hê om e-pos aan ander gebruikers te stuur.", -"emailuser" => "Stuur e-pos na hierdie gebruiker", -"emailpage" => "Stuur e-pos na gebruiker", -"emailpagetext" => "As die gerbuiker 'n geldoge e-posadres in haar of sy gebruikersvoorkeure het, sal die vorm hier onder 'n enkele boodskap stuur. -Die e-posadres wat jy in jou gebruikersvoorkeure het sal verkyn as die \"Van\" adres van die pos, so die ontvanger sal kan terug antwoord.", -"noemailtitle" => "Geen e-posadres", -"noemailtext" => "Hierdie gebruiker het nie 'n geldige e-posadres gespesifiseer nie of het gekies om nie e-pos van ander gebruikers te ontvang nie.", -"emailfrom" => "Van", -"emailto" => "Aan", -"emailsubject" => "Onderwerp", -"emailmessage" => "Boodskap", -"emailsend" => "Stuur", -"emailsent" => "E-pos gestuur", -"emailsenttext" => "Jou e-pos is gestuur.", +'nbytes' => '$1 grepe', +'ncategories' => '$1 {{PLURAL:$1|kategorie|kategorieë}}', +'nlinks' => '$1 skakels', +'nmembers' => '$1 {{PLURAL:$1|lid|lede}}', +'nrevisions' => '$1 {{PLURAL:$1|weergawe|weergawes}}', +'nviews' => '$1 keer aangevra', +'lonelypages' => 'Weesbladsye', +'uncategorizedpages' => 'Bladsye sonder kategorie', +'uncategorizedcategories' => 'Kategoriebladsye wat nie gekategoriseer is nie', +'unusedcategories' => 'Ongebruikte kategorieë', +'unusedimages' => 'Ongebruikte lêers', +'popularpages' => 'Gewilde bladsye', +'wantedcategories' => 'Gesoekte kategorieë', +'wantedpages' => 'Gesogte bladsye', +'mostlinked' => 'Bladsye met meeste skakels daarheen', +'mostlinkedcategories' => 'Kategorieë met die meeste skakels daarheen', +'mostcategories' => 'Artikels met die meeste kategorieë', +'mostimages' => 'Beelde met meeste skakels daarheen', +'mostrevisions' => 'Artikels met meeste wysigings', +'allpages' => 'Alle bladsye', +'randompage' => 'Lukrake bladsy', +'shortpages' => 'Kort bladsye', +'longpages' => 'Lang bladsye', +'deadendpages' => 'Doodloopbladsye', +'listusers' => 'Gebruikerslys', +'specialpages' => 'Spesiale bladsye', +'spheading' => 'Spesiale bladsye', +'rclsub' => '(na bladsye waarna "$1" skakel)', +'newpages' => 'Nuwe bladsye', +'ancientpages' => 'Oudste bladsye', +'move' => 'Skuif', +'movethispage' => 'Skuif hierdie bladsy', +'unusedimagestext' => "

    Let asseblief op dat ander webwerwe, soos die internasionale {{SITENAME}}s, dalk met 'n direkte URL na 'n prent skakel, so die prent sal dus hier verskyn al word dit aktief gebruik.", +'unusedcategoriestext' => 'Die volgende kategoriebladsye bestaan alhoewel geen artikel of kategorie hulle gebruik nie.', + +# Book sources +'booksources' => 'Boekbronne', + +'categoriespagetext' => 'Die volgende kategorieë bestaan op die wiki.', +'version' => 'Weergawe', + +# Special:Log +'specialloguserlabel' => 'Gebruiker:', +'speciallogtitlelabel' => 'Titel:', +'alllogstext' => "Vertoon 'n samestelling van laai-, skrap-, beskerm-, blok- en administrateurlogs van {{SITENAME}}. +Jy kan die vertoning vernou deur 'n logtipe, gebruikersnaam of spesifieke blad te kies.", + +# Special:Allpages +'nextpage' => 'Volgende blad ($1)', +'prevpage' => 'Previous page ($1)', +'allpagesfrom' => 'Wys bladsye vanaf:', +'allarticles' => 'Alle artikels', +'allinnamespace' => 'Alle bladsye (naamruimte $1)', +'allnotinnamespace' => 'Alle bladsye (nie in naamruimte $1 nie)', +'allpagesprev' => 'Vorige', +'allpagesnext' => 'Volgende', +'allpagessubmit' => 'Gaan', +'allpagesprefix' => 'Wys bladsye wat begin met:', + +# E-mail user +'mailnologin' => 'Geen versendadres beskikbaar', +'mailnologintext' => "U moet [[Special:Userlogin|ingeteken]] wees en 'n geldige e-posadres in die [[Special:Preferences|voorkeure]] hê om e-pos aan ander gebruikers te stuur.", +'emailuser' => 'Stuur e-pos na hierdie gebruiker', +'emailpage' => 'Stuur e-pos na gebruiker', +'emailpagetext' => 'As dié gerbuiker \'n geldige e-posadres in sy/haar gebruikersvoorkeure het, sal hierdie vorm \'n enkele boodskap stuur. Die e-posadres in jou gebruikersvoorkeure sal verkyn as die "Van"-adres van die pos. Dus sal die ontvanger kan terug antwoord.', +'defemailsubject' => '{{SITENAME}}-epos', +'noemailtitle' => 'Geen e-posadres', +'noemailtext' => "Hierdie gebruiker het nie 'n geldige e-posadres gespesifiseer nie of het gekies om nie e-pos van ander gebruikers te ontvang nie.", +'emailfrom' => 'Van', +'emailto' => 'Aan', +'emailsubject' => 'Onderwerp', +'emailmessage' => 'Boodskap', +'emailsend' => 'Stuur', +'emailsent' => 'E-pos gestuur', +'emailsenttext' => 'Jou e-pos is gestuur.', # Watchlist -# -"watchlist" => "My dophoulys", -"mywatchlist" => "My dophoulys", -"nowatchlist" => "Jy het geen items in jou dophoulys nie.", -"watchnologin" => "Nie ingeteken nie", -"watchnologintext" => "Jy moet [[Special:Userlogin|ingeteken]] -wees om jou dophoulys te verander.", -"addedwatch" => "Bygevoeg tot dophoulys", -"addedwatchtext" => "Die bladsy \"$1\" is by jou dophoulys gevoeg. -Toekomstige veranderinge aan hierdie bladsye en sy geassosieerde Bespreekbladsy sal hier verskyn en die bladsy sal in vetdruk verskyn in die lys van onlangse wysigings om dit makliker te maak om dit raak te sien. - -As jy die bladsy later van jou dophoulys wil verwyder, kliek \"Moenie meer dophou\" in die kantbalk.", -"removedwatch" => "Afgehaal van dophoulys", -"removedwatchtext" => "Die bladsy \"$1\" is van jou dophoulys afgehaal.", -"watchthispage" => "Hou hierdie bladsy dop", -"unwatchthispage" => "Moenie meer dophou", -"notanarticle" => "Nie 'n artikel", +'watchlist' => 'My dophoulys', +'mywatchlist' => 'My dophoulys', +'watchlistfor' => "(vir '''$1''')", +'nowatchlist' => 'Jy het geen items in jou dophoulys nie.', +'watchnologin' => 'Nie ingeteken nie', +'watchnologintext' => 'Jy moet [[Special:Userlogin|ingeteken]] +wees om jou dophoulys te verander.', +'addedwatch' => 'Bygevoeg tot dophoulys', +'addedwatchtext' => 'Die bladsy "$1" is by u [[Special:Watchlist|dophoulys]] gevoeg. +Die bladsy "$1" is by u [[Special:Watchlist|dophoulys]] gevoeg. Toekomstige veranderinge aan hierdie bladsy en sy verwante besprekingsblad sal daar verskyn en die bladsy sal in \'\'\'vetdruk\'\'\' verskyn in die [[Special:Recentchanges|lys van onlangse wysigings]], sodat u dit makliker kan raaksien. + +As u die bladsy later van u dophoulys wil verwyder, kliek "verwyder van dophoulys" in die kieslys bo-aan die bladsy.', +'removedwatch' => 'Afgehaal van dophoulys', +'removedwatchtext' => 'Die bladsy "[[:$1]]" is van u dophoulys afgehaal.', +'watch' => 'Hou dop', +'watchthispage' => 'Hou hierdie bladsy dop', +'unwatch' => 'Verwyder van dophoulys', +'unwatchthispage' => 'Moenie meer dophou', +'notanarticle' => "Nie 'n artikel", +'watchnochange' => 'Geen item op die dophoulys is geredigeer in die gekose periode nie.', +'watchlistcontains' => 'Jou dophoulys bevat $1 bladsye.', +'wlnote' => 'Hier volg die laaste $1 veranderings binne die laaste $2 ure.', +'wlshowlast' => 'Wys afgelope $1 ure, $2 dae of $3', +'wlsaved' => "Hierdie is 'n gestoorde weergawe van jou waglys.", # Delete/protect/revert -# -"deletepage" => "Skrap bladsy", -"confirm" => "Bevestig", -"confirmdelete" => "Bevestig skrapping", -"deletesub" => "(Besig om \"$1\" te skrap)", -"confirmdeletetext" => "Jy staan op die punt om 'n bladsy of prent asook al hulle geskiedenis uit die databasis te skrap. +'deletepage' => 'Skrap bladsy', +'confirm' => 'Bevestig', +'excontent' => "inhoud was: '$1'", +'excontentauthor' => "Inhoud was: '$1' (en '[[[Spesiaal:Contributions/$2|$2]]' was die enigste bydraer)", +'exbeforeblank' => "Inhoud voor uitwissing was: '$1'", +'exblank' => 'bladsy was leeg', +'confirmdelete' => 'Bevestig skrapping', +'deletesub' => '(Besig om "$1" te skrap)', +'confirmdeletetext' => "Jy staan op die punt om 'n bladsy of prent asook al hulle geskiedenis uit die databasis te skrap. Bevestig asseblief dat jy dit wil doen, dat jy die gevolge verstaan en dat jy dit doen in ooreenstemming met die [[{{MediaWiki:policy-url}}]].", -'policy-url' => 'Project:Policy', -"actioncomplete" => "Aksie uitgevoer", -"deletedtext" => "\"$1\" is geskrap. -Kyk na $2 vir 'n rekord van onlangse skrappings.", -"deletedarticle" => "\"$1\" geskrap", -"dellogpage" => "Skrap_log", -"dellogpagetext" => "Hier onder is 'n lys van die mees onlangse skrappings. Alle tye is bedienertyd (UGT). -

      -
    ", -"deletionlog" => "skrappingslog", -"reverted" => "Het terug gegaan na vroeëre weergawe", -"deletecomment" => "Rede vir skrapping", -"imagereverted" => "Terugkeer na vorige weergawe was suksesvol.", -"rollback" => "Rol veranderinge terug", -"rollbacklink" => "Rol terug", -"cantrollback" => "Kan nie na verandering terug keer nie; die laaste bydraer is die enigste outer van hierdie bladsy.", -"revertpage" => "Het teruggegaan na laaste verandering wat $1 gemaak het", +'actioncomplete' => 'Aksie uitgevoer', +'deletedtext' => '"$1" is geskrap. +Kyk na $2 vir \'n rekord van onlangse skrappings.', +'deletedarticle' => '"$1" geskrap', +'dellogpage' => 'Skraplogboek', +'dellogpagetext' => "Hier onder is 'n lys van die mees onlangse skrappings. Alle tye is bedienertyd (UGT).", +'deletionlog' => 'skrappingslog', +'reverted' => 'Het terug gegaan na vroeëre weergawe', +'deletecomment' => 'Rede vir skrapping', +'imagereverted' => 'Terugkeer na vorige weergawe was suksesvol.', +'rollback' => 'Rol veranderinge terug', +'rollback_short' => 'Rol terug', +'rollbacklink' => 'Rol terug', +'rollbackfailed' => 'Terugrol onsuksesvol', +'cantrollback' => 'Kan nie na verandering terug keer nie; die laaste bydraer is die enigste outer van hierdie bladsy.', +'editcomment' => 'Die wysigopsomming was: "$1".', # only shown if there is an edit comment +'revertpage' => 'Wysigings deur [[Spesiaal:Contributions/$2|$2]] teruggerol na laaste weergawe deur $1', +'protectlogpage' => 'Beskermlogboek', +'protectedarticle' => 'het [[$1]] beskerm', +'unprotectedarticle' => 'het beskerming van [[$1]] verwyder', +'protectsub' => '(Beskerm "$1")', +'confirmprotect' => 'Bevestig beskerming', +'protectcomment' => 'Rede vir beskerming:', +'unprotectsub' => '(Verwyder beskerming van "$1")', +'protect-default' => '(normaal)', +'protect-level-autoconfirmed' => 'Beskerm teen anonieme wysigings', +'protect-level-sysop' => 'Slegs administrateurs', # Undelete -"undelete" => "Herstel geskrapte bladsy", -"undeletepage" => "Bekyk en herstel geskrapte bladsye", -"undeletepagetext" => "Die volgende bladsye is geskrap, maar hulle is nog in die argief en kan herstel word. Die argief kan periodiek skoongemaak word.", -"undeletearticle" => "Herstel geskrapte bladsy", -"undeleterevisions" => "$1 weergawes in argief", -"undeletehistory" => "As jy die bladsy herstel, sal alle weergawes herstel word. +'undelete' => 'Herstel geskrapte bladsy', +'undeletepage' => 'Bekyk en herstel geskrapte bladsye', +'undeletepagetext' => 'Die volgende bladsye is geskrap, maar hulle is nog in die argief en kan herstel word. Die argief kan periodiek skoongemaak word.', +'undeleterevisions' => '$1 weergawes in argief', +'undeletehistory' => "As jy die bladsy herstel, sal alle weergawes herstel word. As 'n nuwe bladsy met dieselfde naam sedert die skrapping geskep is, sal die herstelde weergawes in die nuwe bladsy se voorgeskiedenis verskyn en die huidige weergawe van die lewendige bladsy sal nie outomaties vervang word nie.", -"undeleterevision" => "Geskrape weergawes vanaf $1", -"undeletebtn" => "Herstel!", -"undeletedarticle" => "het \"$1\" herstel", +'undeletebtn' => 'Herstel!', +'undeletedarticle' => 'het "$1" herstel', -# Contributions -# -"contributions" => "Gebruikersbydraes", -"mycontris" => "My bydraes", -"contribsub2" => "Vir $1 ($2)", +# Namespace form on various pages +'namespace' => 'Naamruimte:', +'invert' => 'Omgekeerde seleksie', -"nocontribs" => "Geen veranderinge wat by hierdie kriteria pas, is gevind nie.", -"ucnote" => "Hier onder is die gebruiker se laaste $1 veranderings in die laaste $2 dae.", -"uclinks" => "Bekyk die laaste $1 veranderings; bekyk die laaste $2 dae.", -"uctop" => " (boontoe)" , +# Contributions +'contributions' => 'Gebruikersbydraes', +'mycontris' => 'My bydraes', +'contribsub2' => 'Vir $1 ($2)', +'nocontribs' => 'Geen veranderinge wat by hierdie kriteria pas, is gevind nie.', +'ucnote' => 'Hier volg die gebruiker se laaste $1 wysigings in die afgelope $2 dae.', +'uclinks' => 'Vertoon die laaste $1 veranderings; vertoon die afgelope $2 dae.', +'uctop' => ' (boontoe)', # What links here -# -"whatlinkshere" => "Wat skakel hierheen", -"notargettitle" => "Geen teiken", -"notargettext" => "Jy het nie 'n teikenbladsy of gebruiker waarmee hierdie funksie moet werk, gespesifiseer nie.", -"linklistsub" => "(Lys van skakels)", -"linkshere" => "Die volgende bladsye skakel hierheen:", -"nolinkshere" => "Geen bladsye skakel hierheen nie.", -"isredirect" => "Stuur bladsy aan", - -# Block/unblock IP -# -"blockip" => "Blok IP-adres", -"blockiptext" => "Gebruik die vorm hier onder om skryftoegang van 'n sekere IP-adres te blok. +'whatlinkshere' => 'Wat skakel hierheen', +'notargettitle' => 'Geen teiken', +'notargettext' => "Jy het nie 'n teikenbladsy of gebruiker waarmee hierdie funksie moet werk, gespesifiseer nie.", +'linklistsub' => '(Lys van skakels)', +'linkshere' => 'Die volgende bladsye skakel hierheen:', +'nolinkshere' => 'Geen bladsye skakel hierheen nie.', +'isredirect' => 'Stuur bladsy aan', + +# Block/unblock +'blockip' => 'Blok IP-adres', +'blockiptext' => "Gebruik die vorm hier onder om skryftoegang van 'n sekere IP-adres te blok. Dit moet net gedoen word om vandalisme te voorkom en in ooreenstemming met [[{{MediaWiki:policy-url}}|{{SITENAME}} policy]]. Vul 'n spesifieke rede hier onder in (haal byvoorbeeld spesifieke bladsye wat gevandaliseer is, aan).", -"ipaddress" => "IP-Adres", -"ipbreason" => "Rede", -"ipbsubmit" => "Blok hierdie adres", -"badipaddress" => "Die IP-adres is nie in die regte formaat nie.", -"blockipsuccesssub" => "Blokkering het geslaag", -"blockipsuccesstext" => "Die IP-adres \"$1\" is geblok. -
    Kyk na [[Special:Ipblocklist|IP block list]] vir 'n oorsig van blokkerings.", -"unblockip" => "Maak IP-adres oop", -"unblockiptext" => "Gebruik die vorm hier onder om skryftoegang te herstel vir 'n voorheen geblokkeerde IP-adres.", -"ipusubmit" => "Maak hierdie adres oop", -"ipblocklist" => "Lys van geblokkeerde IP-adresse", -'blocklistline' => '$1, $2 het $3 geblok ($4)', -"blocklink" => "blok", -"unblocklink" => "maak oop", -"contribslink" => "bydraes", +'ipaddress' => 'IP-adres', +'ipadressorusername' => 'IP-adres of gebruikernaam:', +'ipbexpiry' => 'Duur:', +'ipbreason' => 'Rede', +'ipbsubmit' => 'Blok hierdie adres', +'ipbother' => 'Ander tydperk:', +'ipboptions' => '2 ure:2 hours,1 dag:1 day,3 dae:3 days,1 week:1 week,2 weke:2 weeks,1 maand:1 month,3 maande:3 months,6 maande:6 months,1 jaar:1 year,onbeperk:infinite', +'ipbotheroption' => 'ander', +'badipaddress' => 'Die IP-adres is nie in die regte formaat nie.', +'blockipsuccesssub' => 'Blokkering het geslaag', +'blockipsuccesstext' => 'Die IP-adres "$1" is geblok. +
    Kyk na [[Special:Ipblocklist|IP block list]] vir \'n oorsig van blokkerings.', +'unblockip' => 'Maak IP-adres oop', +'unblockiptext' => "Gebruik die vorm hier onder om skryftoegang te herstel vir 'n voorheen geblokkeerde IP-adres.", +'ipusubmit' => 'Maak hierdie adres oop', +'ipblocklist' => 'Lys van geblokkeerde IP-adresse', +'blocklistline' => '$1, $2 het $3 geblok ($4)', +'infiniteblock' => 'oneindig', +'blocklink' => 'blok', +'unblocklink' => 'maak oop', +'contribslink' => 'bydraes', +'blocklogpage' => 'Blokkeerlogboek', +'unblocklogentry' => 'blokkade van $1 is opgehef:', +'proxyblocker' => 'Proxyblokker', # Developer tools -# -"lockdb" => "Sluit databasis", -"unlockdb" => "Ontsluit databasis", -"lockdbtext" => "As jy die databasis sluit, kan geen gebruiker meer bladsye redigeer nie, voorkeure verander nie, dophoulyste verander nie, of ander aksies uitvoer wat veranderinge in die databasis verg nie. -Bevestig asseblief dat dit is wat jy wil doen en dat jy die databasis sal ontsluit sodra jy jou instandhouding afgehandel het.", -"unlockdbtext" => "As jy die databasis ontsluit, kan gebruikers weer bladsye redigeer, voorkeure verander, dophoulyste verander, of ander aksies uitvoer wat veranderinge in die databasis verg. -Bevestig asseblief dat dit is wat jy wil doen.", -"lockconfirm" => "Ja, ek wil regtig die databasis sluit.", -"unlockconfirm" => "Ja, ek wil regtig die databasis ontsluit.", -"lockbtn" => "Sluit die databasis", -"unlockbtn" => "Ontsluit die databasis", -"locknoconfirm" => "Jy het nie die bevestigblokkie gemerk nie.", -"lockdbsuccesssub" => "Databasissluit het geslaag", -"unlockdbsuccesssub" => "Databasisslot is verwyder", -"lockdbsuccesstext" => "Die {{SITENAME}} databasis is gesluit. -
    Onthou om dit te ontsluit wanneer jou onderhoud afgehandel is.", -"unlockdbsuccesstext" => "Die {{SITENAME}} databasis is ontsluit.", +'lockdb' => 'Sluit databasis', +'unlockdb' => 'Ontsluit databasis', +'lockdbtext' => 'As jy die databasis sluit, kan geen gebruiker meer bladsye redigeer nie, voorkeure verander nie, dophoulyste verander nie, of ander aksies uitvoer wat veranderinge in die databasis verg nie. +Bevestig asseblief dat dit is wat jy wil doen en dat jy die databasis sal ontsluit sodra jy jou instandhouding afgehandel het.', +'unlockdbtext' => 'As jy die databasis ontsluit, kan gebruikers weer bladsye redigeer, voorkeure verander, dophoulyste verander, of ander aksies uitvoer wat veranderinge in die databasis verg. +Bevestig asseblief dat dit is wat jy wil doen.', +'lockconfirm' => 'Ja, ek wil regtig die databasis sluit.', +'unlockconfirm' => 'Ja, ek wil regtig die databasis ontsluit.', +'lockbtn' => 'Sluit die databasis', +'unlockbtn' => 'Ontsluit die databasis', +'locknoconfirm' => 'Jy het nie die bevestigblokkie gemerk nie.', +'lockdbsuccesssub' => 'Databasissluit het geslaag', +'unlockdbsuccesssub' => 'Databasisslot is verwyder', +'lockdbsuccesstext' => 'Die {{SITENAME}} databasis is gesluit. +
    Onthou om dit te ontsluit wanneer jou onderhoud afgehandel is.', +'unlockdbsuccesstext' => 'Die {{SITENAME}}-databasis is ontsluit.', # Move page -# -"movepage" => "Skuif bladsy", -"movepagetext" => "Met die vorm hier onder kan jy 'n bladsy hernoem en so al sy geskiedenis na die nuwe naam skuif. -Die ou titel sal 'n aanstuurbladsy na die nuwe titel word. -Skakels na die ou bladsytitel sal nie verander nie; maak seker dat jy -check vir dubbele of gebrekte aansture. -Dis jou verantwoordelikheid om seker te maak dat skakels steeds wys waarheen hulle moet. +'movepage' => 'Skuif bladsy', +'movepagetext' => "Die vorm hieronder hernoem 'n bladsy en skuif die hele wysigingsgeskiedenis na die nuwe naam. +Die ou bladsy sal vervang word met 'n aanstuurblad na die nuwe titel. +'''Skakels na die ou bladsytitel sal nie outomaties verander word nie; maak seker dat dubbele aanstuurverwysings nie voorkom nie deur die \"wat skakel hierheen\"-funksie na die skuif te gebruik.''' Dit is jou verantwoordelikheid om seker te maak dat skakels steeds wys na waarheen hulle behoort te gaan. -Let op dat 'n bladsy '''nie''' geskuif sal word as daar reeds 'n bladsy met so 'n titel bestaan nie, tensy dit leeg is off 'n aanstuurbladsy is, en dit het geen veranderingsgeskiedenis nie. Dit beteken dat jy 'n bladsy kan hernoem na sy ou titel as jy 'n fout gemaak het, en jy kan nie oor 'n bestaande bladsy skryf nie. +Let daarop dat 'n bladsy '''nie''' geskuif sal word indien daar reeds 'n bladsy met dieselfde titel bestaan nie, tensy dit leeg of 'n aanstuurbladsy is en geen wysigingsgeskiedenis het nie. Dit beteken dat jy 'n bladsy kan terugskuif na sy ou titel indien jy 'n fout gemaak het, maar jy kan nie 'n bestaande bladsy oorskryf nie. WAARSKUWING! -Hierdie kan 'n drasitiese en onverwagte verandering vir 'n populêre bladsy wees; -maak asseblief seker dat jy die gevolge verstaan voordat jy voortgaan.", -"movepagetalktext" => "Die geassosieerde praatbladsy, indien enige, sal outomaties saam met dit geskuif word, '''behalwe as:''' -*Jy die bladsy oor naamkontekste heen skuif, -*'n Bespreekbladsy wat nie leeg is nie reeds onder die nuwe naam bestaan, of -*Jy die merk uit blokkie hier onder wegneem. - -In hierdie gevalle, sal jy die bladsy met die hand moet skuif of saamsmelt as jy wil.", -"movearticle" => "Skuif bladsy", -"movenologin" => "Nie ingeteken nie", -"movenologintext" => "Jy moet 'n geregistreerde gebruiker wees en [[Special:Userlogin|ingeteken]] +Hierdie kan 'n drastiese en onverwagte verandering vir 'n populêre bladsy wees; +maak asseblief seker dat jy die gevolge van hierdie aksie verstaan voordat jy voortgaan. Gebruik ook die ooreenstemmende besprekingsbladsy om oorleg te pleeg met ander bydraers.", +'movearticle' => 'Skuif bladsy', +'movenologin' => 'Nie ingeteken nie', +'movenologintext' => "Jy moet 'n geregistreerde gebruiker wees en [[Special:Userlogin|ingeteken]] wees om 'n bladsy te skuif.", -"newtitle" => "Na nuwe titel", -"movepagebtn" => "Skuif bladsy", -"pagemovedsub" => "Verskuiwing het geslaag", -"pagemovedtext" => "Bladsy \"[[$1]]\" geskuif na \"[[$2]]\".", -"articleexists" => "'n Bladsy met daardie naam bestaan reeds, of die naam wat jy gekies het, is nie geldig nie. +'newtitle' => 'Na nuwe titel', +'movepagebtn' => 'Skuif bladsy', +'pagemovedsub' => 'Verskuiwing het geslaag', +'movepage-moved' => '\'\'\'"$1" geskuif na "$2"\'\'\'', # The two titles are passed in plain text as $3 and $4 to allow additional goodies in the message. +'articleexists' => "'n Bladsy met daardie naam bestaan reeds, of die naam wat jy gekies het, is nie geldig nie. Kies asseblief 'n ander naam.", -"talkexists" => "Die bladsy self is suksesvol verskuif, maar die bespreekbladsy kon nie geskuif word nie omdat een reeds bestaan met die nuwe titel. Smelt hulle asseblief met die hand saam.", -"movedto" => "geskuif na", -"movetalk" => "Skuif \"bespreek\"bladsy ook, indien van toepassing.", -"talkpagemoved" => "Die ooreenkomstige bespreekbladsy is ook geskuif.", -"talkpagenotmoved" => "Die ooreenkomstige bespreekbladsy is nie geskuif nie.", - -#Math -'mw_math_png' => "Gebruik altyd PNG.", -'mw_math_simple' => "Gebruik HTML indien dit eenvoudig is, andersins PNG.", -'mw_math_html' => "Gebruik HTML wanneer moontlik, andersins PNG.", -'mw_math_source' => "Los as TeX (vir teks blaaiers).", -'mw_math_modern' => "Moderne blaaiers.", +'talkexists' => "'''Die bladsy self is suksesvol geskuif, maar die besprekingsbladsy is nie geskuif nie omdat een reeds bestaan met die nuwe titel. Smelt hulle asseblief met die hand saam.'''", +'movedto' => 'geskuif na', +'movetalk' => 'Skuif besprekingsblad ook, indien van toepassing.', +'talkpagemoved' => 'Die ooreenkomstige besprekingsblad is ook geskuif.', +'talkpagenotmoved' => 'Die ooreenkomstige besprekingsblad is nie geskuif nie.', +'1movedto2' => '[[$1]] geskuif na [[$2]]', +'1movedto2_redir' => '[[$1]] geskuif na [[$2]] oor bestaande aanstuur', +'movelogpage' => 'Skuiflogboek', +'movereason' => 'Rede:', +'selfmove' => 'Bron- en teikentitels is dieselfde; kan nie bladsy oor homself skuif nie.', + +# Export +'export' => 'Eksporteer bladsye', + +# Namespace 8 related +'allmessages' => 'Alle stelselboodskappe', +'allmessagesname' => 'Naam', +'allmessagesdefault' => 'Verstekteks', +'allmessagescurrent' => 'Huidige teks', +'allmessagestext' => "Hierdie is \'n lys boodskappe wat beskikbaar is in die ''MediaWiki''-naamspasie.", +'allmessagesfilter' => 'Boodskapnaamfilter:', +'allmessagesmodified' => 'Wys slegs gewysigdes', + +# Tooltip help for the actions +'tooltip-search' => 'Deursoek {{SITENAME}}', +'tooltip-minoredit' => "Dui aan hierdie is 'n klein wysiging", +'tooltip-save' => 'Stoor jou wysigings', +'tooltip-preview' => "Sien 'n voorskou van jou wysigings, gebruik voor jy die blad stoor!", +'tooltip-compareselectedversions' => 'Vergelyk die twee gekose weergawes van hierdie blad.', +'tooltip-watch' => 'Voeg hierdie blad by jou dophoulys', + +# Attribution +'anonymous' => 'Anonieme gebruiker(s) van {{SITENAME}}', +'siteuser' => '{{SITENAME}} gebruiker $1', +'and' => 'en', +'siteusers' => '{{SITENAME}} gebruiker(s) $1', + +# Spam protection +'subcategorycount' => 'Daar is {{PLURAL:$1|een subkategorie|$1 subkategorieë}} onder hierdie kategorie.', +'categoryarticlecount' => 'Daar is {{PLURAL:$1|een artikel|$1 artikels}} in hierdie kategorie.', +'listingcontinuesabbrev' => 'vervolg', + +# Math options +'mw_math_png' => 'Gebruik altyd PNG.', +'mw_math_simple' => 'Gebruik HTML indien dit eenvoudig is, andersins PNG.', +'mw_math_html' => 'Gebruik HTML wanneer moontlik, andersins PNG.', +'mw_math_source' => 'Los as TeX (vir teksblaaiers).', +'mw_math_modern' => 'Moderne blaaiers.', 'mw_math_mathml' => 'MathML', -); +# Browsing diffs +'previousdiff' => '← Ouer wysiging', +'nextdiff' => 'Nuwer wysiging →', + +# Media information +'imagemaxsize' => 'Beperk beelde op beeldbeskrywingsbladsye tot:', + +'newimages' => 'Gallery van nuwe beelde', +'noimages' => 'Niks te sien nie.', + +'passwordtooshort' => 'U wagwoord is te kort. Dit moet ten minste $1 karakters hê.', + +'exif-lightsource-10' => 'Bewolkte weer', + +# External editor support +'edit-externally' => "Wysig hierdie lêer met 'n eksterne program", +'edit-externally-help' => 'Sien die [http://meta.wikimedia.org/wiki/Help:External_editors instruksies] (in Engels) vir meer inligting.', +# 'all' in various places, this might be different for inflected languages +'recentchangesall' => 'alle', +'imagelistall' => 'alle', +'watchlistall2' => 'alles', +'namespacesall' => 'alle', +'monthsall' => 'alle', + +# E-mail address confirmation +'confirmemail' => 'Bevestig e-posadres', +'confirmemail_text' => "Hierdie wiki vereis dat u e-posadres bevestig word voordat epos-funksies gebruik word. Klik onderstaande knoppie om 'n bevestigingspos na u adres te stuur. Die pos sal 'n skakel met 'n kode insluit; maak hierdie skakel oop in u webblaaier om te bevestig dat die adres geldig is.", +'confirmemail_send' => "Pos 'n bevestigingkode", +'confirmemail_sent' => 'Bevestigingpos gestuur.', +'confirmemail_invalid' => 'Ongeldige bevestigingkode. Die kode het moontlik verval.', +'confirmemail_success' => 'U e-posadres is bevestig. U kan nou aanteken en die wiki gebruik.', +'confirmemail_loggedin' => 'U e-posadres is nou bevestig.', +'confirmemail_error' => 'Iets het foutgegaan met die stoor van u bevestiging.', +'confirmemail_subject' => '{{SITENAME}}: E-posadres-bevestiging', +'confirmemail_body' => 'Iemand, waarskynlik van u IP-adres ($1), het \'n rekening "$2" geregistreer met hierdie e-posadres by {{SITENAME}}. + +Om te bevestig dat hierdie adres werklik aan u behoort, en om die posfasiliteite by {{SITENAME}} te aktiveer, besoek hierdie skakel in u blaaier: + +$3 + +Indien dit nié u was nie, ignoreer bloot die skakel (en hierdie pos). Hierde bevestigingkode verval om $4.', + +# Delete conflict +'confirmrecreate' => "Gebruiker [[User:$1|$1]] ([[User talk:$1|bespreek]]) het hierdie blad uitgevee ná u begin redigeer het met rede: : ''$2'' +Bevestig asseblief dat u regtig hierdie blad oor wil skep.", + +# HTML dump +'redirectingto' => 'Stuur aan na [[$1]]...', + +# action=purge +'confirm_purge' => 'Verwyder die kas van hierdie blad? + +$1', +'confirm_purge_button' => 'Regso', + +'articletitles' => "Artikels wat met ''$1'' begin", + +# Auto-summaries +'autosumm-blank' => 'Alle inhoud uit bladsy verwyder', +'autosumm-replace' => "Vervang bladsyinhoud met '$1'", +'autoredircomment' => 'Stuur aan na [[$1]]', +'autosumm-new' => 'Nuwe blad: $1', + +); -?> diff --git a/languages/messages/MessagesAn.php b/languages/messages/MessagesAn.php index a10cd232..1f1694bc 100644 --- a/languages/messages/MessagesAn.php +++ b/languages/messages/MessagesAn.php @@ -21,4 +21,4 @@ $namespaceNames = array( NS_CATEGORY_TALK => 'Descusión_categoría', ); -?> + diff --git a/languages/messages/MessagesAr.php b/languages/messages/MessagesAr.php index 7c8e5fec..5037c71b 100644 --- a/languages/messages/MessagesAr.php +++ b/languages/messages/MessagesAr.php @@ -14,6 +14,67 @@ $defaultUserOptionOverrides = array( # Underlines seriously harm legibility. Force off: 'underline' => 0, ); +/** + * A list of date format preference keys which can be selected in user + * preferences. New preference keys can be added, provided they are supported + * by the language class's timeanddate(). Only the 5 keys listed below are + * supported by the wikitext converter (DateFormatter.php). + * + * The special key "default" is an alias for either dmy or mdy depending on + * $wgAmericanDates + */ +$datePreferences = array( + 'default', + 'mdy', + 'dmy', + 'ymd', + 'ISO 8601', +); + +/** + * The date format to use for generated dates in the user interface. + * This may be one of the above date preferences, or the special value + * "dmy or mdy", which uses mdy if $wgAmericanDates is true, and dmy + * if $wgAmericanDates is false. + */ +$defaultDateFormat = 'dmy or mdy'; + +/** + * Associative array mapping old numeric date formats, which may still be + * stored in user preferences, to the new string formats. + */ +$datePreferenceMigrationMap = array( + 'default', + 'mdy', + 'dmy', + 'ymd' +); + +/** + * These are formats for dates generated by MediaWiki (as opposed to the wikitext + * DateFormatter). Documentation for the format string can be found in + * Language.php, search for sprintfDate. + * + * This array is automatically inherited by all subclasses. Individual keys can be + * overridden. + */ +$dateFormats = array( + 'mdy time' => 'H:i', + 'mdy date' => 'xg j، Y', # Arabic comma + 'mdy both' => 'H:i، xg j، Y', # Arabic comma + + 'dmy time' => 'H:i', + 'dmy date' => 'j xg Y', + 'dmy both' => 'H:i، j xg Y', # Arabic comma + + 'ymd time' => 'H:i', + 'ymd date' => 'Y xg j', + 'ymd both' => 'H:i، Y xg j', # Arabic comma + + 'ISO 8601 time' => 'xnH:xni:xns', + 'ISO 8601 date' => 'xnY-xnm-xnd', + 'ISO 8601 both' => 'xnY-xnm-xnd"T"xnH:xni:xns', +); $namespaceNames = array( NS_MEDIA => 'ملف', @@ -43,7 +104,6 @@ $magicWords = array( 'forcetoc' => array( 0, "__لصق_فهرس__", "__FORCETOC__" ), 'toc' => array( 0, "__فهرس__", "__TOC__" ), 'noeditsection' => array( 0, "__لاتحريرقسم__", "__NOEDITSECTION__" ), - 'start' => array( 0, "__ابدأ__", "__START__" ), 'currentmonth' => array( 1, "شهر", "شهر_حالي", "CURRENTMONTH" ), 'currentmonthname' => array( 1, "اسم_شهر", "اسم_شهر_حالي", "CURRENTMONTHNAME" ), 'currentday' => array( 1, "يوم", "CURRENTDAY" ), @@ -58,23 +118,30 @@ $magicWords = array( 'localday' => array( 1, "يوم_محلي", "LOCALDAY" ), 'localday2' => array( 1, "يوم_محلي2", "LOCALDAY2" ), 'localdayname' => array( 1, "اسم_يوم_محلي", "LOCALDAYNAME" ), - 'localyear' => array( 1, "عام_محلية", "LOCALYEAR" ), + 'localyear' => array( 1, "عام_محلي", "LOCALYEAR" ), 'localtime' => array( 1, "وقت_محلي", "LOCALTIME" ), 'localhour' => array( 1, "ساعة_محلية", "LOCALHOUR" ), 'numberofpages' => array( 1, "عددالصفحات", "عدد_الصفحات", "NUMBEROFPAGES" ), 'numberofarticles' => array( 1, "عددالمقالات", "عدد_المقالات", "NUMBEROFARTICLES" ), 'numberoffiles' => array( 1, "عددالملفات", "عدد_الملفات", "NUMBEROFFILES" ), 'numberofusers' => array( 1, "عددالمستخدمين", "عدد_المستخدمين", "NUMBEROFUSERS" ), - 'pagename' => array( 1, "اسم_صفحة", "PAGENAME" ), - 'pagenamee' => array( 1, "عنوان_صفحة", "PAGENAMEE" ), + 'numberofedits' => array( 1, "عددالتعديلات", "عدد_التعديلات", "NUMBEROFEDITS" ), + 'pagename' => array( 1, "اسم_صفحة", "اسم_الصفحة", "PAGENAME" ), + 'pagenamee' => array( 1, "عنوان_صفحة", "عنوان_الصفحة", "PAGENAMEE" ), 'namespace' => array( 1, "نطاق", "NAMESPACE" ), 'namespacee' => array( 1, "عنوان_نطاق", "NAMESPACEE" ), 'talkspace' => array( 1, "نطاق_نقاش,نطاق_النقاش", "TALKSPACE" ), 'talkspacee' => array( 1, "عنوان_نقاش,عنوان_النقاش", "TALKSPACEE" ), - 'fullpagename' => array( 1, "اسم_كامل", "FULLPAGENAME" ), - 'fullpagenamee' => array( 1, "عنوان_كامل", "FULLPAGENAMEE" ), + 'fullpagename' => array( 1, "اسم_الصفحة_الكامل", "اسم_صفحة_كامل", "FULLPAGENAME" ), + 'fullpagenamee' => array( 1, "عنوان_الصفحة_الكامل", "عنوان_صفحة_كامل", "FULLPAGENAMEE" ), + 'subpagename' => array( 1, "اسم_الصفحة_الفرعي", "اسم_صفحة_فرعي", "SUBPAGENAME" ), + 'subpagenamee' => array( 1, "عنوان_الصفحة_الفرعي", "عنوان_صفحة_فرعي", "SUBPAGENAMEE" ), + 'basepagename' => array( 1, "اسم_الصفحة_الأساسي", "اسم_صفحة_أساسي", "BASEPAGENAME" ), + 'basepagenamee' => array( 1, "عنوان_الصفحة_الأساسي", "عنوان_صفحة_أساسي", "BASEPAGENAMEE" ), + 'talkpagename' => array( 1, "اسم_صفحة_النقاش", "TALKPAGENAME" ), + 'talkpagenamee' => array( 1, "عنوان_صفحة_النقاش", "TALKPAGENAMEE" ), 'msg' => array( 0, "رسالة:", "MSG:" ), - 'subst' => array( 0, "نسخ:", "نسخ_قالب:", "SUBST:" ), + 'subst' => array( 0, "نسخ:", "SUBST:" ), 'msgnw' => array( 0, "مصدر:", "مصدر_قالب:", "MSGNW:" ), 'img_thumbnail' => array( 1, "تصغير", "thumbnail", "thumb" ), 'img_manualthumb' => array( 1, "تصغير=$1", "thumbnail=$1", "thumb=$1" ), @@ -82,83 +149,101 @@ $magicWords = array( 'img_left' => array( 1, "يسار", "left" ), 'img_none' => array( 1, "بدون,بلا", "none" ), 'img_width' => array( 1, "$1بك", "$1px" ), - 'img_center' => array( 1, "وسط", "center", "centre" ), - 'img_framed' => array( 1, "إطار", "اطار", "framed", "enframed", "frame" ), + 'img_center' => array( 1, "مركز", "center", "centre" ), + 'img_framed' => array( 1, "إطار", "framed", "enframed", "frame" ), + 'img_frameless' => array( 1, "لاإطار", "frameless" ), 'img_page' => array( 1, "صفحة=$1", "صفحة $1", "page=$1", "page $1" ), + 'img_border' => array( 1, "حد", "حدود", "border" ), + 'img_top' => array( 1, "أعلى", "top" ), + 'img_text_top' => array( 1, "نص_أعلى", "text-top" ), + 'img_middle' => array( 1, "وسط", "middle" ), + 'img_bottom' => array( 1, "أسفل", "bottom" ), + 'img_text_bottom' => array( 1, "نص_أسفل", "text-bottom" ), 'int' => array( 0, "محتوى:", "INT:" ), - 'sitename' => array( 1, "اسم_الموقع", "SITENAME" ), + 'sitename' => array( 1, "اسم_الموقع", "اسم_موقع", "SITENAME" ), 'ns' => array( 0, "نط:", "NS:" ), 'localurl' => array( 0, "عنوان:", "LOCALURL:" ), - 'server' => array( 0, "العنوان", "SERVER" ), - 'servername' => array( 0, "اسم_عنوان", "SERVERNAME" ), + 'server' => array( 0, "الخادم", "خادم", "SERVER" ), + 'servername' => array( 0, "اسم_الخادم", "اسم_خادم", "SERVERNAME" ), 'scriptpath' => array( 0, "مسار", "SCRIPTPATH" ), 'notitleconvert' => array( 0, "لاتحويل_عنوان", "__NOTITLECONVERT__", "__NOTC__" ), 'nocontentconvert' => array( 0, "لاتحويل_محتوى", "__NOCONTENTCONVERT__", "__NOCC__" ), 'currentweek' => array( 1, "أسبوع", "CURRENTWEEK" ), 'currentdow' => array( 1, "رقم_يوم_أسبوع", "CURRENTDOW" ), 'localweek' => array( 1, "أسبوع_محلي", "LOCALWEEK" ), + 'localdow' => array( 1, "يوم_محلي_مأ", "LOCALDOW" ), 'revisionid' => array( 1, "نسخة", "REVISIONID" ), + 'revisionday' => array( 1, "يوم_النسخة", "يوم_نسخة", "REVISIONDAY" ), + 'revisionmonth' => array( 1, "شهر_النسخة", "شهر_نسخة", "REVISIONMONTH" ), + 'revisionyear' => array( 1, "سنة_النسخة", "عام_النسخة", "REVISIONYEAR" ), 'plural' => array( 0, "جمع:", "PLURAL:" ), 'fullurl' => array( 0, "عنوان_كامل:", "FULLURL:" ), - 'newsectionlink' => array( 1, "__رابط_قسم_جديد__", "__NEWSECTIONLINK__" ), + 'newsectionlink' => array( 1, "__وصلة_قسم_جديد__", "__NEWSECTIONLINK__" ), + 'currentversion' => array( 1, "نسخة_حالية", "CURRENTVERSION" ), + 'currenttimestamp' => array( 1, "طابع_الوقت_الحالي", "CURRENTTIMESTAMP" ), + 'localtimestamp' => array( 1, "طابع_الوقت_المحلي", "LOCALTIMESTAMP" ), 'language' => array( 0, "#لغة:", "#LANGUAGE:" ), - 'numberofadmins' => array( 1, "عدد_إداريين", "عدد_الإداريين", "NUMBEROFADMINS" ), + 'contentlanguage' => array( 1, "لغة_المحتوى", "لغة_محتوى", "CONTENTLANGUAGE", "CONTENTLANG" ), + 'pagesinnamespace' => array( 1, "صفحات_في_نطاق", "PAGESINNAMESPACE:", "PAGESINNS:" ), + 'numberofadmins' => array( 1, "عددالإداريين", "عدد_الإداريين", "NUMBEROFADMINS" ), 'special' => array( 0, "خاص", "special" ), + 'defaultsort' => array( 1, "ترتيب_قياسي", "ترتيب_افتراضي", "DEFAULTSORT:", "DEFAULTSORTKEY:", "DEFAULTCATEGORYSORT:" ), ); $skinNames = array( - 'standard' => array( "كلاسيك" ), - 'nostalgia' => array( "نوستالجيا" ), - 'cologneblue' => array( "كولون بلو" ), - 'davinci' => array( "دافنشي" ), - 'mono' => array( "مونو" ), - 'monobook' => array( "مونوبوك" ), - 'myskin' => array( "واجهتي" ), - 'chick' => array( "تشيك" ), + 'standard' => "كلاسيك", + 'nostalgia' => "نوستالجيا", + 'cologneblue' => "كولون بلو", + 'monobook' => "مونوبوك", + 'myskin' => "واجهتي", + 'chick' => "تشيك", + 'simple' => "سيمبل", ); $digitTransformTable = array( - '0' => '٠', - '1' => '١', - '2' => '٢', - '3' => '٣', - '4' => '٤', - '5' => '٥', - '6' => '٦', - '7' => '٧', - '8' => '٨', - '9' => '٩', - '.' => '٫', // wrong table? - ',' => '٬' + '0' => '٠', # ٠ + '1' => '١', # ١ + '2' => '٢', # ٢ + '3' => '٣', # ٣ + '4' => '٤', # ٤ + '5' => '٥', # ٥ + '6' => '٦', # ٦ + '7' => '٧', # ٧ + '8' => '٨', # ٨ + '9' => '٩', # ٩ + '.' => '٫', # ٫ wrong table ? + ',' => '٬', # ٬ ); $specialPageAliases = array( 'DoubleRedirects' => array( "تحويلات_مزدوجة" ), 'BrokenRedirects' => array( "تحويلات_مكسورة" ), 'Disambiguations' => array( "توضيحات" ), - 'Userlogin' => array( "دخول" ), - 'Userlogout' => array( "خروج" ), + 'Userlogin' => array( "دخول_المستخدم" ), + 'Userlogout' => array( "خروج_المستخدم" ), 'Preferences' => array( "تفضيلات" ), - 'Watchlist' => array( "المراقبة" ), + 'Watchlist' => array( "قائمة_المراقبة" ), 'Recentchanges' => array( "أحدث_التغييرات" ), 'Upload' => array( "رفع" ), 'Imagelist' => array( "قائمة_الصور" ), 'Newimages' => array( "صور_جديدة" ), 'Listusers' => array( "قائمة_المستخدمين" ), - 'Statistics' => array( "احصاءات" ), + 'Statistics' => array( "إحصاءات" ), 'Randompage' => array( "عشوائي", "صفحة_عشوائية" ), 'Lonelypages' => array( "صفحات_يتيمة" ), 'Uncategorizedpages' => array( "صفحات_غير_مصنفة" ), 'Uncategorizedcategories' => array( "تصنيفات_غير_مصنفة" ), 'Uncategorizedimages' => array( "صور_غير_مصنفة" ), + 'Uncategorizedtemplates' => array( "قوالب_غير_مصنفة" ), 'Unusedcategories' => array( "تصنيفات_غير_مستخدمة" ), 'Unusedimages' => array( "صور_غير_مستخدمة" ), 'Wantedpages' => array( "صفحات_مطلوبة" ), 'Wantedcategories' => array( "تصنيفات_مطلوبة" ), 'Mostlinked' => array( "الأكثر_وصلا" ), - 'Mostlinkedcategories' => array( "التصنيفات_الأكبر" ), - 'Mostcategories' => array( "الصفحات_الأكثر_تصنيفا" ), - 'Mostimages' => array( "الصور_الأكثر_وصلا" ), + 'Mostlinkedcategories' => array( "أكثر_التصنيفات_وصلا" ), + 'Mostlinkedtemplates' => array( "أكثر_القوالب_وصلا" ), + 'Mostcategories' => array( "أكثر_الصفحات_تصنيفا" ), + 'Mostimages' => array( "أكثر_الصور_وصلا" ), 'Mostrevisions' => array( "الأكثر_تعديلا" ), 'Fewestrevisions' => array( "الأقل_تعديلا" ), 'Shortpages' => array( "صفحات_قصيرة" ), @@ -172,7 +257,7 @@ $specialPageAliases = array( 'Ipblocklist' => array( "قائمة_منع_أيبي" ), 'Specialpages' => array( "صفحات_خاصة" ), 'Contributions' => array( "مساهمات" ), - 'Emailuser' => array( "رسالة_إلكترونية" ), + 'Emailuser' => array( "راسل_المستخدم" ), 'Whatlinkshere' => array( "ماذا_يصل" ), 'Recentchangeslinked' => array( "تغييرات_مرتبطة" ), 'Movepage' => array( "نقل_صفحة" ), @@ -180,7 +265,7 @@ $specialPageAliases = array( 'Booksources' => array( "مصدر_كتاب" ), 'Categories' => array( "تصنيفات" ), 'Export' => array( "تصدير" ), - 'Version' => array( "النسخة" ), + 'Version' => array( "إصدار" ), 'Allmessages' => array( "كل_الرسائل" ), 'Log' => array( "سجل" ), 'Blockip' => array( "منع_أيبي" ), @@ -191,10 +276,10 @@ $specialPageAliases = array( 'Userrights' => array( "صلاحيات" ), 'MIMEsearch' => array( "بحث_MIME" ), 'Unwatchedpages' => array( "صفحات_غيرمراقبة" ), - 'Listredirects' => array( "التحويلات" ), - 'Revisiondelete' => array( "إلغاء_تعديل" ), + 'Listredirects' => array( "عرض_التحويلات" ), + 'Revisiondelete' => array( "حذف_تعديل" ), 'Unusedtemplates' => array( "قوالب_غير_مستخدمة" ), - 'Randomredirect' => array( "تحويل_عشوائي" ), + 'Randomredirect' => array( "تحويلة_عشوائية" ), 'Mypage' => array( "صفحتي" ), 'Mytalk' => array( "نقاشي" ), 'Mycontributions' => array( "مساهماتي" ), @@ -209,62 +294,62 @@ $messages = array( # User preference toggles 'tog-underline' => 'خط تحت الوصلات', 'tog-highlightbroken' => 'إظهار الوصلات المكسورة بهذا الشكل (البديل بهذا الشكل؟).', -'tog-justify' => 'اجعل عرض الأسطر في الفقرات متساوٍ', +'tog-justify' => 'اجعل عرض الأسطر في الفقرات متساو', 'tog-hideminor' => 'خبئ التعديلات الطفيفة في أحدث التغييرات', -'tog-extendwatchlist' => 'تمديد قائمة المراقبة لإظهار جميع التغيرات المطبقة', -'tog-usenewrc' => 'شكل متقدم من صفحة أحدث التغييرات (يتطلب جافا سكريبت)', -'tog-numberheadings' => 'رقّم الأقسام تلقائيًا', -'tog-showtoolbar' => 'إظهار شريط التحرير (يتطلب جافا سكريبت)', -'tog-editondblclick' => 'اذهب إلى صفحة التحرير عند النقر المزدوج على المقالة (يتطلب جافا سكريبت)', -'tog-editsection' => 'تمكين تعديل الأقسام في المقال عن طريق وصلات [تحرير]', -'tog-editsectiononrightclick' => 'مكّن تحرير الأقسام في المقال عن طريق كبسة الفأرة اليمين على عناوين الأقسام (يتطلب جافا سكريبت)', -'tog-showtoc' => 'إظهار فهرس المحتويات (للمقالات التي تحتوي على أكثر من 3 أقسام)', -'tog-rememberpassword' => 'تذكر عبر الجلسات', +'tog-extendwatchlist' => 'مدد قائمة المراقبة لإظهار جميع التغييرات المطبقة', +'tog-usenewrc' => 'شكل متقدم من صفحة أحدث التغييرات (جافا سكريبت)', +'tog-numberheadings' => 'رقّم الأقسام تلقائيا', +'tog-showtoolbar' => 'اعرض شريط التحرير (جافا سكريبت)', +'tog-editondblclick' => 'عدل الصفحات عند النقر المزدوج على الصفحة (جافا سكريبت)', +'tog-editsection' => 'مكن تعديل الأقسام عن طريق وصلات [تحرير]', +'tog-editsectiononrightclick' => 'مكّن تحرير الأقسام في عن طريق كبسة الفأرة اليمين
    على عناوين الأقسام (يتطلب جافا سكريبت)', +'tog-showtoc' => 'عرض فهرس المحتويات (للصفحات التي تحتوي على أكثر من 3 أقسام)', +'tog-rememberpassword' => 'تذكر بيانات دخولي على هذا الحاسوب', 'tog-editwidth' => 'صندوق التحرير يأخذ كامل عرض المساحة المتاحة', -'tog-watchcreations' => 'أضف الصفحات التي أنشأها إلى قائمة مراقبتي', -'tog-watchdefault' => 'أضف الصفحات التي أقوم بتحريرها إلى قائمة مراقبتي', +'tog-watchcreations' => 'أضف الصفحات التي أنشئها إلى قائمة مراقبتي', +'tog-watchdefault' => 'أضف الصفحات التي أحررها إلى قائمة مراقبتي', 'tog-watchmoves' => 'أضف الصفحات التي أنقلها إلى قائمة مراقبتي', -'tog-watchdeletion' => 'أضف الصفحات التي أمسحها إلى قائمة مراقبتي', +'tog-watchdeletion' => 'أضف الصفحات التي أحذفها إلى قائمة مراقبتي', 'tog-minordefault' => "خيار ''هذا تعديل طفيف'' هو الخيار الافتراضي", 'tog-previewontop' => 'إظهار العرض المسبق قبل صندوق التحرير', 'tog-previewonfirst' => 'اعرض الصفحة عند حفظها بعد أول تعديل', 'tog-nocache' => 'امنع حفظ نسخة من الصفحة بعد زيارتها', -'tog-enotifwatchlistpages' => 'أرسل لي رسالة بالتغييرات على الصفحة', -'tog-enotifusertalkpages' => 'أرسل لي رسالة عندما يتم تحرير صفحة نقاشي', -'tog-enotifminoredits' => 'أرسل لي رسالة للتغييرات الطفيفة أيضًا', +'tog-enotifwatchlistpages' => 'أرسل لي رسالة عندما يتم تغيير صفحة أراقبها', +'tog-enotifusertalkpages' => 'أرسل لي رسالة عندما يتم تغيير صفحة نقاشي', +'tog-enotifminoredits' => 'أرسل لي رسالة للتعديلات الطفيفة أيضا', 'tog-enotifrevealaddr' => 'إظهار عنوان بريدي الإلكتروني في رسائل التنويه', -'tog-shownumberswatching' => 'إظهار عدد المستخدمين المراقبين للصفحة', -'tog-fancysig' => '
    فقط توقيع (بدون وصلة أوتوماتيكية)', -'tog-externaleditor' => 'استخدم محرر نصوص خارجي تلقائيًا', -'tog-externaldiff' => 'استخدام برنامج خارجي لعرض الفروق', -'tog-showjumplinks' => 'تفعيل وصلات "اذهب إلى".', +'tog-shownumberswatching' => 'اعرض عدد المستخدمين المراقبين للصفحة', +'tog-fancysig' => 'فقط توقيع (بدون وصلة أوتوماتيكية)', +'tog-externaleditor' => 'استخدم محرر نصوص خارجيا تلقائيا', +'tog-externaldiff' => 'استخدام فروقا خارجية افتراضيا', +'tog-showjumplinks' => 'فعل وصلات "اذهب إلى".', 'tog-uselivepreview' => 'استخدم الاستعراض السريع (جافاسكريبت) (تجريبي)', -'tog-forceeditsummary' => 'نبهني عند تركي خانة "الملخص" فارغة', +'tog-forceeditsummary' => 'نبهني عند إدخال ملخص تحرير فارغ', 'tog-watchlisthideown' => 'أخف تعديلاتي من قائمة المراقبة', 'tog-watchlisthidebots' => 'أخف تعديلات البوت من قائمة المراقبة', -'tog-watchlisthideminor' => 'إخفاء التعديلات الطفيفة من قائمة مراقبتي', -'tog-nolangconversion' => 'تعطيل تحويل المتغيرات', -'tog-ccmeonemails' => 'إرسال نسخ من الرسائل التي أرسلها للمستخدمين الآخرين إلى بريدي الخاص', -'tog-diffonly' => 'لا تعرض محتوى الصفحة عند المقارنة بين التعديلات.', +'tog-watchlisthideminor' => 'أخف التعديلات الطفيفة من قائمة المراقبة', +'tog-nolangconversion' => 'عطّل تحويل المتغيرات', +'tog-ccmeonemails' => 'أرسل لي نسخا من رسائل البريد الإلكتروني التي أرسلها للمستخدمين الآخرين', +'tog-diffonly' => 'لا تعرض محتوى الصفحة عند المقارنة بين التعديلات', -'underline-always' => 'دائمًا', +'underline-always' => 'دائما', 'underline-never' => 'لا', -'underline-default' => 'تبعًا لإعدادات المتصفح', +'underline-default' => 'تبعا لإعدادات المتصفح', 'skinpreview' => '(عرض)', # Dates 'sunday' => 'الأحد', -'monday' => 'الاثنين', +'monday' => 'الإثنين', 'tuesday' => 'الثلاثاء', 'wednesday' => 'الأربعاء', 'thursday' => 'الخميس', 'friday' => 'الجمعة', 'saturday' => 'السبت', 'sun' => 'أحد', -'mon' => 'اثنين', +'mon' => 'إثنين', 'tue' => 'ثلاثاء', -'wed' => 'الأربعاء', +'wed' => 'أربعاء', 'thu' => 'خميس', 'fri' => 'جمعة', 'sat' => 'سبت', @@ -306,113 +391,114 @@ $messages = array( 'dec' => 'ديسمبر', # Bits of text used by many pages -'categories' => '{{PLURAL:$1|تصنيف الصفحة|تصنيفات الصفحة}}', -'pagecategories' => '{{PLURAL:$1|تصنيف الصفحة|تصنيفات الصفحة}}', +'categories' => 'تصنيفات', +'pagecategories' => '{{PLURAL:$1|تصنيف|تصنيفات}}', 'category_header' => 'المقالات في التصنيف "$1"', 'subcategories' => 'التصنيفات الفرعية', -'category-media-header' => 'الملفات في التصنيف "$1"', +'category-media-header' => 'ملفات الميديا في التصنيف "$1"', +'category-empty' => "''هذا التصنيف لا يحتوي حاليا على مقالات أو ملفات ميديا.''", 'mainpagetext' => "'''تم تشغيل ميدياويكي بنجاح.'''", -'mainpagedocfooter' => 'أنظر [http://meta.wikimedia.org/wiki/Help:Contents دليل المستخدم] لمعلومات حول استخدام الويكي. +'mainpagedocfooter' => 'انظر [http://meta.wikimedia.org/wiki/Help:Contents دليل المستخدم] لمعلومات حول استخدام الويكي. == البداية == -* [http://www.mediawiki.org/wiki/Help:Configuration_settings قائمة إعدادات الضبط] -* [http://www.mediawiki.org/wiki/Help:FAQ اسئلة متكررة حول ميدياويكي] -* [http://mail.wikimedia.org/mailman/listinfo/mediawiki-announce القائمة البريدية الخاصة باصدار ميدياويكي]', +* [http://www.mediawiki.org/wiki/Manual:Configuration_settings قائمة إعدادات الضبط] +* [http://www.mediawiki.org/wiki/Manual:FAQ اسئلة متكررة حول ميدياويكي] +* [http://lists.wikimedia.org/mailman/listinfo/mediawiki-announce القائمة البريدية الخاصة بإصدار ميدياويكي]', 'about' => 'حول', 'article' => 'مقالة', -'newwindow' => '(يفتح في شباك جديد)', -'cancel' => 'الغ', +'newwindow' => '(تفتح في نافذة جديدة)', +'cancel' => 'إلغاء', 'qbfind' => 'بحث', 'qbbrowse' => 'تصفح', -'qbedit' => 'تحرير', +'qbedit' => 'عدل', 'qbpageoptions' => 'هذه الصفحة', 'qbpageinfo' => 'سياق النص', 'qbmyoptions' => 'صفحاتي', -'qbspecialpages' => 'الصفحات الخاصّة', -'moredotdotdot' => 'مزيد...', +'qbspecialpages' => 'الصفحات الخاصة', +'moredotdotdot' => 'المزيد...', 'mypage' => 'صفحتي', -'mytalk' => 'صفحة نقاشي', -'anontalk' => 'تناقش مع عنوان الأيبي هذا', -'navigation' => 'تصفح', +'mytalk' => 'نقاشي', +'anontalk' => 'النقاش لعنوان الأيبي هذا', +'navigation' => 'إبحار', # Metadata in edit box -'metadata_help' => 'معطيات ميتا:', +'metadata_help' => 'بيانات ميتا:', 'errorpagetitle' => 'خطأ', -'returnto' => 'الرجوع إلى $1.', +'returnto' => 'ارجع إلى $1.', 'tagline' => 'من {{SITENAME}}', 'help' => 'مساعدة', 'search' => 'بحث', 'searchbutton' => 'بحث', -'go' => 'إذهب', -'searcharticle' => 'إذهب', -'history' => 'التاريخ', -'history_short' => 'التاريخ', -'updatedmarker' => 'تم تعديل الصفحة بعد آخر زيارة', +'go' => 'اذهب', +'searcharticle' => 'اذهب', +'history' => 'تاريخ الصفحة', +'history_short' => 'تاريخ', +'updatedmarker' => 'تم تعديل الصفحة بعد زيارتي الأخيرة', 'info_short' => 'معلومات', 'printableversion' => 'نسخة للطباعة', 'permalink' => 'وصلة دائمة', 'print' => 'طباعة', -'edit' => 'تعديل', +'edit' => 'عدل', 'editthispage' => 'عدل هذه الصفحة', 'delete' => 'حذف', -'deletethispage' => 'حذف هذه الصفحة', +'deletethispage' => 'احذف هذه الصفحة', 'undelete_short' => 'استرجاع {{PLURAL:$1|تعديل واحد|تعديلان|$1 تعديلات|$1 تعديل|$1 تعديلا}}', 'protect' => 'حماية', 'protect_change' => 'تغيير مستوى الحماية', -'protectthispage' => 'حماية هذه الصفحة', +'protectthispage' => 'احم هذه الصفحة', 'unprotect' => 'إزالة الحماية', -'unprotectthispage' => 'إزالة الحماية عن الصفحة', +'unprotectthispage' => 'إزالة حماية هذه الصفحة', 'newpage' => 'صفحة جديدة', 'talkpage' => 'ناقش هذه الصفحة', -'talkpagelinktext' => 'النقاش', +'talkpagelinktext' => 'نقاش', 'specialpage' => 'صفحة خاصة', 'personaltools' => 'أدوات شخصية', -'postcomment' => 'أرسل تعليق', -'articlepage' => 'عرض المقالة', -'talk' => 'النقاش', +'postcomment' => 'أرسل تعليقا', +'articlepage' => 'عرض صفحة المحتوى', +'talk' => 'نقاش', 'views' => 'معاينة', 'toolbox' => 'أدوات', 'userpage' => 'عرض صفحة المستخدم', -'projectpage' => 'عرض الصفحة العامة', +'projectpage' => 'عرض صفحة المشروع', 'imagepage' => 'عرض صفحة الصورة', 'mediawikipage' => 'عرض صفحة الرسالة', 'templatepage' => 'عرض صفحة القالب', 'viewhelppage' => 'عرض صفحة المساعدة', 'categorypage' => 'عرض صفحة التصنيف', 'viewtalkpage' => 'عرض النقاش', -'otherlanguages' => ' لغات أخرى', +'otherlanguages' => 'بلغات أخرى', 'redirectedfrom' => '(تم التحويل من $1)', 'redirectpagesub' => 'صفحة تحويل', -'lastmodifiedat' => 'آخر تعديل لهذه الصفحة كان في $2, $1.', # $1 date, $2 time -'viewcount' => 'تم عرض هذه الصفحة {{plural:$1|مرة واحدة|مرتان|$1 مرات|$1 مرة}}.', +'lastmodifiedat' => 'آخر تعديل لهذه الصفحة كان في $2، $1.', # $1 date, $2 time +'viewcount' => 'تم عرض هذه الصفحة {{PLURAL:$1|مرة واحدة|مرتان|$1 مرات|$1 مرة}}.', 'protectedpage' => 'صفحة محمية', 'jumpto' => 'اذهب إلى:', -'jumptonavigation' => 'تصفح', +'jumptonavigation' => 'إبحار', 'jumptosearch' => 'بحث', # All link text and link target definitions of links into project namespace that get used by other message strings, with the exception of user group pages (see grouppage) and the disambiguation template definition (see disambiguations). 'aboutsite' => 'حول {{SITENAME}}', 'aboutpage' => 'Project:حول', 'bugreports' => 'تقارير الأخطاء', -'bugreportspage' => 'Project:بلاغ_أخطاء', +'bugreportspage' => 'Project:بلاغ أخطاء', 'copyright' => 'المحتويات تحت $1.', 'copyrightpagename' => 'حقوق النسخ في {{SITENAME}}', 'copyrightpage' => 'Project:حقوق النسخ', 'currentevents' => 'الأحداث الجارية', 'currentevents-url' => 'الأحداث الجارية', 'disclaimers' => 'عدم مسؤولية', -'disclaimerpage' => 'Project:عدم_مسؤولية_عام', +'disclaimerpage' => 'Project:عدم مسؤولية عام', 'edithelp' => 'مساعدة التحرير', -'edithelppage' => 'Project:مساعدة التحرير', +'edithelppage' => 'مساعدة:تحرير', 'faq' => 'الأسئلة الأكثر تكرارا', 'faqpage' => 'Project:أسئلة متكررة', 'helppage' => 'Help:محتويات', 'mainpage' => 'الصفحة الرئيسية', -'policy-url' => '{{ns:project}}:سياسة', +'policy-url' => 'Project:سياسة', 'portal' => 'بوابة المجتمع', 'portal-url' => 'Project:بوابة المجتمع', 'privacy' => 'سياسة الخصوصية', @@ -421,49 +507,51 @@ $messages = array( 'sitesupport-url' => 'Project:دعم الموقع', 'badaccess' => 'خطأ في الصلاحيات المطلوبة', -'badaccess-group0' => 'ليس من المسموح لك تنفيذ ما طلبت.', -'badaccess-group1' => 'العمل الذي طلبتموه ينحصر في مجموعه مستخدمي $1.', -'badaccess-group2' => 'العمل طلبتموه محدود للمستخدمين في احدى المجموعات $1 .', -'badaccess-groups' => 'الأمر الذي طلبته مسموح به فقط من خلال أحد الأعضاء في المجموعات $1.', +'badaccess-group0' => 'ليس من المسموح لك تنفيذ الأمر الذي طلبته.', +'badaccess-group1' => 'الأمر الذي طلبته مقصور على المستخدمين في المجموعة $1.', +'badaccess-group2' => 'الأمر الذي طلبته مقصور على المستخدمين في إحدى المجموعتين $1 .', +'badaccess-groups' => 'الأمر الذي طلبته مقصور على المستخدمين في إحدى المجموعات $1.', 'versionrequired' => 'يلزم الإصدار $1 من ميدياويكي', 'versionrequiredtext' => 'يلزم الإصدار رقم $1 من ميدياويكي لاستعمال هذه الصفحة. انظر [[Special:Version|رقم الإصدار]]', -'ok' => 'موافق', -'pagetitle' => '$1 - {{SITENAME}}', -'retrievedfrom' => 'تمّ الاسترجاع من "$1"', -'youhavenewmessages' => 'يوجد لديك $1 ($2).', -'newmessageslink' => 'رسائل جديدة', -'newmessagesdifflink' => 'آخر تغيير', -'editsection' => 'تحرير', -'editold' => 'تحرير', -'editsectionhint' => 'تحرير القسم: $1', -'toc' => 'فهرست', -'showtoc' => 'إظهار', -'hidetoc' => 'إخفاء', -'thisisdeleted' => 'هل تريد استعراض أو استرجاع $1؟', -'viewdeleted' => 'عرض $1؟', -'restorelink' => '{{PLURAL:$1|تعديل واحد محذوف|تعديلان محذوفان|$1 تعديلات محذوفة|$1 تعديل محذوف|$1 تعديلا محذوفا}}', -'feedlinks' => 'تلقيم:', -'feed-invalid' => 'نوع إشتراك التليقم خاطئ.', +'ok' => 'موافق', +'retrievedfrom' => 'تم الاسترجاع من "$1"', +'youhavenewmessages' => 'توجد لديك $1 ($2).', +'newmessageslink' => 'رسائل جديدة', +'newmessagesdifflink' => 'آخر تغيير', +'youhavenewmessagesmulti' => 'لديك رسائل جديدة على $1', +'editsection' => 'تحرير', +'editold' => 'تحرير', +'editsectionhint' => 'تحرير القسم: $1', +'toc' => 'فهرست', +'showtoc' => 'إظهار', +'hidetoc' => 'إخفاء', +'thisisdeleted' => 'هل تريد استعراض أو استرجاع $1؟', +'viewdeleted' => 'عرض $1؟', +'restorelink' => '{{PLURAL:$1|تعديل واحد محذوف|تعديلان محذوفان|$1 تعديلات محذوفة|$1 تعديل محذوف|$1 تعديلا محذوفا}}', +'feedlinks' => 'تلقيم:', +'feed-invalid' => 'نوع اشتراك التليقم خاطئ.', +'feed-atom' => 'أتوم', +'feed-rss' => 'أر إس إس', # Short words for each namespace, by default used in the 'article' tab in monobook 'nstab-main' => 'مقالة', 'nstab-user' => 'صفحة مستخدم', 'nstab-media' => 'صفحة ميديا', 'nstab-special' => 'خاص', -'nstab-project' => 'حول', +'nstab-project' => 'صفحة مشروع', 'nstab-image' => 'ملف', 'nstab-mediawiki' => 'رسالة', 'nstab-template' => 'قالب', -'nstab-help' => 'مساعدة', +'nstab-help' => 'صفحة مساعدة', 'nstab-category' => 'تصنيف', # Main script and global functions 'nosuchaction' => 'لا يوجد أمر كهذا', 'nosuchactiontext' => 'الأمر في مسار الصفحة لم يتم التعرف عليه من خلال الويكي', 'nosuchspecialpage' => 'لا توجد صفحة خاصة بهذا الاسم', -'nospecialpagetext' => 'لقد طلبت صفحة خاصة لا يمكن التعرف عليها من قبل نظام الويكي.', +'nospecialpagetext' => 'لقد طلبت صفحة خاصة غير صحيحة، قائمة الصفحات الخاصة الصحيحة موجودة في[[Special:Specialpages|قائمة الصفحات الخاصة]].', # General errors 'error' => 'خطأ', @@ -479,64 +567,69 @@ MySQL أرجعت الخطأ "$3: $4".', "$1" من داخل الدالة "$2". MySQL أرجعت الخطأ "$3: $4"', -'noconnect' => 'عفوا! تعاني الويكي من بعض المشاكل التقنية, و لا يمكن الاتصال بخادم قاعدة البيانات.
    +'noconnect' => 'عفوا! تعاني الويكي من بعض المشاكل التقنية، و لا يمكن الاتصال بخادم قاعدة البيانات.
    $1', 'nodb' => 'لم يتمكن من اختيار قاعدة البيانات $1', -'cachederror' => 'هذه نسخة مخبأة من الصفحة المطلوبة, و قد لا تكون محدثة.', +'cachederror' => 'هذه نسخة مخبأة من الصفحة المطلوبة، و قد لا تكون محدثة.', 'laggedslavemode' => 'تحذير: هذه الصفحة قد لا تحتوي على أحدث التغييرات.', 'readonly' => 'قاعدة البيانات مغلقة', -'enterlockreason' => 'اذكر سبب المنع والوقت المتوقع لإزالة المنع', +'enterlockreason' => 'اذكر سبب المنع والوقت المتوقع لرفع المنع', 'readonlytext' => 'قاعدة البيانات مغلقة حاليا أمام الإضافات والتعديلات، السبب غالبا ما يكون الصيانة، وستعود قاعدة البيانات للوضع الطبيعي قريبا. -عندما تم أغلاق قاعدة البيانات أمام التعديلات والإضافات تم أعطاء السبب التالي: +الإداري الذي أغلق قاعدة البيانات أعطى التفسير التالي:

    $1', 'missingarticle' => 'لم تعثر قاعدة البيانات على نص الصفحة المفترض، باسم "$1". -عادة ما يكون السبب في ذلك اتّباع صفحة فروق قديمة أو وصلة لتاريخ صفحة محذوفة. إذا كنت تعتقد أن السبب غير ذلك، فمن الممكن أن يكون هنالك علة في البرنامج. الرجاء إعلام الإداريين عن هذا الخطأ مع ذكر الوصلة.', +عادة ما يكون السبب في ذلك اتباع وصلة فرق قديمة أو وصلة لتاريخ صفحة محذوفة. إذا كنت تعتقد أن السبب غير ذلك، فمن الممكن أن تكون هناك علة في البرنامج. الرجاء إعلام أحد الإداريين عن هذا الخطأ مع ذكر الوصلة.', +'readonly_lag' => 'تم إغلاق قاعدة البيانات تلقائيا حتى تستطيع الخوادم التابعة ملاحقة الخادم الرئيسي', 'internalerror' => 'خطأ داخلي', +'internalerror_info' => 'خطأ داخلي: $1', 'filecopyerror' => 'لا يمكن نسخ الملف من "$1" إلى "$2".', 'filerenameerror' => 'لا يمكن تغيير اسم الملف من "$1" إلى "$2".', 'filedeleteerror' => 'لا يمكن حذف الملف "$1".', +'directorycreateerror' => 'لم يمكن إنشاء المجلد "$1".', 'filenotfound' => 'لا يمكن إيجاد الملف "$1".', +'fileexistserror' => 'غير قادر على الكتابة للملف "$1": الملف موجود', 'unexpected' => 'قيمة غير متوقعة: "$1"="$2".', -'formerror' => 'خلل: طلبك لم ينفذ', -'badarticleerror' => 'لا يمكن إجراء هذا الطلب على هذه الصفحة.', -'cannotdelete' => 'لم يتم مسح الصفحة أو الملف المطلوب. ( ربما يكون قد تم مسحه من قبل مستخدم آخر. )', +'formerror' => 'خطأ: طلبك لم ينفذ', +'badarticleerror' => 'لا يمكن إجراء هذا الأمر على هذه الصفحة.', +'cannotdelete' => 'لم يتم حذف الصفحة أو الملف المطلوب. (ربما يكون قد تم حذفه من قبل مستخدم آخر. )', 'badtitle' => 'عنوان خاطئ', 'badtitletext' => 'عنوان الصفحة المطلوبة إما خاطئ أو فارغ، و ربما الوصلة بين اللغات أو بين المشاريع خاطئة. ومن الممكن وجود رموز لا تصلح للاستعمال في العنوان.', -'perfdisabled' => 'عفوا! هذه الخاصية معطله حاليا لإنها تؤثر بشكل سلبي على أداء الويكي و تبطئها إلى حد استحالة استخدامها.', -'perfdisabledsub' => 'هذه نسخة محفوظة مسبقا من $1:', # obsolete? -'perfcached' => 'القائمة التالية تمّ تخزينها في وقت سابق وقد لا تكون محدّثة بعد:', +'perfdisabled' => 'عفوا! هذه الخاصية معطله حاليا لأنها تؤثر بشكل سلبي على أداء الويكي و تبطئها إلى حد استحالة استخدامها.', +'perfcached' => 'القائمة التالية تم تخزينها في وقت سابق وقد لا تكون محدثة بعد:', 'perfcachedts' => 'البيانات التالية مخزنة سابقا، آخر تحديث لها كان في $1.', -'querypage-no-updates' => 'تم تعطيل عمليات التحديث والإضافة لهذه الصفحة في الوقت الحالي. البيانات المعروضة هنا لن يتم تحديثها الأن.', +'querypage-no-updates' => 'تم تعطيل عمليات التحديث والإضافة لهذه الصفحة في الوقت الحالي. البيانات المعروضة هنا لن يتم تحديثها الآن.', 'wrong_wfQuery_params' => 'معاملات خاطئة في wfQuery()
    الدالة: $1
    الاستعلام: $2', -'viewsource' => 'عرض المصدر للمقالة', +'viewsource' => 'عرض المصدر', 'viewsourcefor' => 'ل $1', -'protectedpagetext' => "'''تمت حماية هذه الصفحة لمنع التعديلات.'''", +'protectedpagetext' => 'تمت حماية هذه الصفحة لمنع التعديلات.', 'viewsourcetext' => 'يمكنك مشاهدة ونسخ مصدر هذه الصفحة:', 'protectedinterface' => 'هذه الصفحة تتضمن نص الواجهة للبرنامج ولقد تم إغلاقها لمنع التخريب.', -'editinginterface' => "'''تحذير''': أنت تقوم بتحرير صفحة تستخدم في الواجهة النصية للبرنامج. أية تغييرات لهذه الصفحة سوف تظهر في الواجهة لجميع المستخدمين الآخرين.", +'editinginterface' => "'''تحذير''': أنت تقوم بتحرير صفحة تستخدم في الواجهة النصية للبرنامج؛ أية تغييرات لهذه الصفحة سوف تظهر في لواجهة المستخدم للمستخدمين الآخرين.", 'sqlhidden' => '(استعلام SQL مخبئ)', -'cascadeprotected' => 'تمت حماية هذه الصفحة من التعديل بسبب كونها مدمجة في الصفحات التالية، والتي تم استعمال خاصية "حماية الصفحات المدمجة" بها:', +'cascadeprotected' => 'تمت حماية هذه الصفحة من التعديل بسبب كونها مدمجة في {{PLURAL:$1|صفحة|صفحات}} التالية، والتي تم استعمال خاصية "حماية الصفحات المدمجة" بها:', +'namespaceprotected' => "لا تمتلك الصلاحية لتعديل الصفحات في نطاق '''$1'''.", +'customcssjsprotected' => 'لا تمتلك الصلاحية لتعديل هذه الصفحة، لأنها تحتوي على الإعدادات الشخصية لمستخدم آخر.', +'ns-specialprotected' => 'الصفحات في النطاق {{ns:special}} لا يمكن تعديلها.', # Login and logout pages 'logouttitle' => 'خروج المستخدم', -'logouttext' => 'أنت الآن غير مسجل الدخول للنظام. -تستطيع المتابعة باستعمال {{SITENAME}} كمجهول، أو الدخول مرة أخرى بنفس الاسم أو باسم آخر. من الممكن أن ترى بعض الصفحات في كما وأنك مسجل في النظام.، وذلك بسبب استعمال الصفحات المخبأة في المتصفح لديك.', -'welcomecreation' => '== أهلا بك يا $1! == -تم إنشاء حسابك بنجاح. لا تنسى أن تقوم بتغيير وتحديد تفضيلاتك في {{SITENAME}}.', +'logouttext' => 'أنت الآن غير مسجل الدخول للنظام.
    +تستطيع المتابعة باستعمال {{SITENAME}} كمجهول، أو الدخول مرة أخرى بنفس الاسم أو باسم آخر. من الممكن أن ترى بعض الصفحات في كما وأنك مسجل في النظام، وذلك بسبب استعمال الصفحات المخبأة في المتصفح لديك.', +'welcomecreation' => '== مرحبا يا $1! == +تم إنشاء حسابك بنجاح؛ لا تنس أن تقوم بتغيير تفضيلاتك في {{SITENAME}}.', 'loginpagetitle' => 'دخول المستخدم', -'yourname' => 'اسم المستخدم', -'yourpassword' => 'كلمة السر', -'yourpasswordagain' => 'أعد كتابة كلمة السر', -'remembermypassword' => 'تذكر كلمة السر عبر الجلسات.', -'yourdomainname' => 'النطاق الخاص بك', +'yourname' => 'اسم المستخدم:', +'yourpassword' => 'كلمة السر:', +'yourpasswordagain' => 'أعد كتابة كلمة السر:', +'remembermypassword' => 'تذكر دخولي على هذا الحاسوب', +'yourdomainname' => 'نطاقك:', 'externaldberror' => 'هناك إما خطأ في دخول قاعدة البيانات الخارجية أو أنه غير مسموح لك بتحديث حسابك الخارجي.', 'loginproblem' => 'حدثت مشكلة أثناء الدخول.
    يرجى المحاولة مرى أخرى!', -'alreadyloggedin' => '$1، أنت مسجل للدخول من قبل!
    ', 'login' => 'دخول', -'loginprompt' => 'يجب أن يدعم متصفحك الكوكيز لتتمكن من الدخول.', +'loginprompt' => 'يجب أن يدعم متصفحك الكوكيز لتتمكن من الدخول إلى {{SITENAME}}.', 'userlogin' => 'دخول / إنشاء حساب', 'logout' => 'خروج', 'userlogout' => 'خروج', @@ -548,68 +641,69 @@ $1', 'gotaccountlink' => 'دخول', 'createaccountmail' => 'عبر البريد الإلكتروني', 'badretype' => 'كلمات السر التي أدخلتها غير متطابقة.', -'userexists' => 'اسم المستخدم الذي إخترته مستخدم من قبل، يرجى إختيار اسم مستخدم آخر.', -'youremail' => 'بريدك الإلكتروني*', +'userexists' => 'اسم المستخدم الذي أدخلته مستعمل من قبل. يرجى اختيار اسم مختلف.', +'youremail' => 'البريد الإلكتروني:', 'username' => 'اسم المستخدم:', 'uid' => 'رقم المستخدم:', -'yourrealname' => 'اسمك الحقيقي*', -'yourlanguage' => 'اللغة', -'yourvariant' => 'مختلف', -'yournick' => 'اللقب الخاص بك (للتواقيع)', +'yourrealname' => 'الاسم الحقيقي:', +'yourlanguage' => 'اللغة:', +'yourvariant' => 'متغير', +'yournick' => 'اللقب:', 'badsig' => 'صيغة توقيع خاطئة؛ تحقق من بيانات الHTML.', +'badsiglength' => 'التوقيع طويل جدا؛ يجب أن يكون أقل من $1 حرف.', 'email' => 'البريد الإلكتروني', -'prefs-help-email-enotif' => 'يستخدم هذا العنوان أيضا في إرسال تنبيهات البريد الإلكتروني إليك لو قمت بتفعيل تلك الخيارات.', -'prefs-help-realname' => '* الاسم الحقيقي (اختياري): لو اخترت أن تعرض اسمك هنا, سيستخدم هذا في الإشارة إلى مساهماتك.', +'prefs-help-realname' => 'الاسم الحقيقي اختياري و لو اخترت أن تعرض اسمك هنا سيستخدم هذا في الإشارة إلى مساهماتك.', 'loginerror' => 'خطأ في الدخول', -'prefs-help-email' => '* البريد الإلكتروني هو مدخل اختياري يمكّن الآخرين من الاتصال بك عن طريق صفحتك أو صفحة نقاشك من دون معرفة عنوانك.', -'nocookiesnew' => 'تم إنشاء حساب المستخدم و لكنك لست مسجل الدخول بعد. يستخدم {{SITENAME}} كوكيز لتسجيل الدخول. لديك الكوكيز معطلة. من فضلك فعلها, ثم أدخل باسم الحساب و كلمة السر الجديدتين.', -'nocookieslogin' => '{{SITENAME}} يستخدم الكوكيز لتسجيل الدخول. لديك الكوكيز معطلة. من فضلك فعلها, ثم أدخل باسم الحساب و كلمة السر الجديدتين. -n.', +'prefs-help-email' => 'البريد الإلكتروني هو مدخل اختياري يمكّن الآخرين من الاتصال بك عن طريق صفحتك أو صفحة نقاشك من دون معرفة هويتك.', +'nocookiesnew' => 'تم إنشاء حساب المستخدم، و لكنك لست مسجل الدخول بعد. يستخدم {{SITENAME}} كوكيز لتسجيل الدخول. لديك الكوكيز معطلة. من فضلك فعلها، ثم أدخل باسم الحساب و كلمة السر الجديدتين.', +'nocookieslogin' => '{{SITENAME}} يستخدم الكوكيز لتسجيل الدخول؛ لديك الكوكيز معطلة؛ من فضلك فعلها ثم حاول مرة أخرى.', 'noname' => 'لم تحدد اسم مستخدم صحيح.', 'loginsuccesstitle' => 'تم الدخول بشكل صحيح', -'loginsuccess' => 'لقد قمت بتسجيل الدخول ل{{SITENAME}} باسم "$1".', +'loginsuccess' => "'''لقد قمت بتسجيل الدخول ل{{SITENAME}} باسم \"\$1\".'''", 'nosuchuser' => 'لا يوجد مستخدم بالاسم "$1". -تأكد من إملاء الاسم، أو استعمل النموذج الموجود في الأسفل لإنشاء مستخدم جديد.', -'nosuchusershort' => 'لا يوجد مستخدم باسم "$1"، تأكد من كتابة الاسم الذي تريده بالطريقة الصحيحة.', -'nouserspecified' => 'يجب عليك تحديد اسم المستخدم', -'wrongpassword' => 'كلمة السر التي أدخلتها غير صحيحة، يرجى إعادة المحاولة.', -'wrongpasswordempty' => 'لقد أدخلت كلمة سر فارغة. حاول مرة أخرى.', +تأكد من إملاء الاسم، أو قم بإنشاء حساب جديد.', +'nosuchusershort' => 'لا يوجد مستخدم باسم "$1". تأكد من إملاء الاسم.', +'nouserspecified' => 'يجب عليك تحديد اسم مستخدم.', +'wrongpassword' => 'كلمة السر التي أدخلتها غير صحيحة. من فضلك حاول مرة أخرى.', +'wrongpasswordempty' => 'كلمة السر المدخلة كانت فارغة. من فضلك حاول مرة أخرى.', +'passwordtooshort' => 'كلمة السر التي اخترتها قصيرة. يجب ألا يقل طول الكلمة عن $1 حرف و أن تكون مختلفة عن اسم المستخدم الخاص بك.', 'mailmypassword' => 'أرسل لي كلمة السر عبر البريد الإلكتروني.', 'passwordremindertitle' => 'تذكير بكلمة السر من {{SITENAME}}', -'passwordremindertext' => 'لقد طلب شخص ما (غالبًا أنت، من عنوان الأيبي $1) أن نرسل لك كلمة سر جديدة لـ{{SITENAME}} ($4). +'passwordremindertext' => 'لقد طلب شخص ما (غالبا أنت، من عنوان الأيبي $1) أن نرسل لك كلمة سر جديدة لـ{{SITENAME}} ($4). -كلمة السرّ الآن للمستخدم "$2" هي "$3". +كلمة السر الآن للمستخدم "$2" هي "$3". عليك أن تقوم بالدخول إلى الموقع وتغيير كلمة السر خاصتك الآن إلا إذا لم تكن أنت من طلب هذا، اقرأ أدناه في هذه الحالة. -إذا لم تكن أنت من قام بطلب كلمة السرّ أو أنك تذكرّت كلمة السرّ السابقة ولا ترغب بتغييرها فبإمكانك أن تتغاضى عن هذه الرسالة وأن تستمر في استخدام كلمة السرّ السابقة خاصتك.', -'noemail' => 'لا يوجد أي عنوان بريدي مسجل للمستخدم "$1".', +إذا لم تكن أنت من قام بطلب كلمة السر أو أنك تذكرت كلمة السر السابقة ولا ترغب بتغييرها فبإمكانك أن تتغاضى عن هذه الرسالة وأن تستمر في استخدام كلمة السر السابقة خاصتك.', +'noemail' => 'لا يوجد أي عنوان بريد إلكتروني مسجل للمستخدم "$1".', 'passwordsent' => 'تم إرسال كلمة سر جديدة إلى العنوان البريدي المسجل للمستخدم "$1". يرجى محاولة تسجيل الدخول مرة أخرى عند استلامها.', -'blocked-mailpassword' => 'تم منع العنوان الخاص بك من التحرير، ولمنع التخريب لا يمكنك أن تستخدم خاصية استرجاع كلمة السر.', -'eauthentsent' => 'تم إرسال رسالة إلكترونية إلى العنوان المسمى. حتى ترسل أي رسالة أخرى لذلك الحساب عليك أن تتبع التعليمات الواردة في الرسالة لتأكيد أن هذا العنوان هو لك بالفعل.', +'blocked-mailpassword' => 'تم منع عنوان الأيبي الخاص بك من التحرير، ولمنع التخريب لا يمكنك أن تستخدم خاصية استرجاع كلمة السر.', +'eauthentsent' => 'تم إرسال رسالة تأكيد إلكترونية إلى العنوان المسمى. حتى ترسل أي رسالة أخرى لذلك الحساب عليك أن تتبع التعليمات الواردة في الرسالة لتأكيد أن هذا الحساب هو لك بالفعل.', 'throttled-mailpassword' => 'تم بالفعل إرسال تذكير بكلمة السر، في خلال الـ$1 ساعة الماضية. لمنع التخريب، تذكير واحد فقط سيتم إرساله كل $1 ساعة.', 'mailerror' => 'خطأ أثناء إرسال البريد: $1', 'acct_creation_throttle_hit' => 'معذرة، لقد قمت بإنشاء $1 حساب. لا يمكنك عمل المزيد.', 'emailauthenticated' => 'تم تأكيد بريدك الإلكتروني على $1.', -'emailnotauthenticated' => 'لم يتمّ التحقق من بريدك الإلكتروني. لن يتمّ إرسال رسائل لأي من الميزات التالية.', -'noemailprefs' => 'يجب إعطاء بريد إلكتروني لتفعيل هذه الخصائص.', -'emailconfirmlink' => 'أكد بريدك الإلكتروني', -'invalidemailaddress' => 'لا يمكن قبول العنوان الإلكتروني الذي زودته حيث يبدو خاطئا. من فضلك ضع عنوانا مضبوطا أو أفرغ هذا الحقل.', +'emailnotauthenticated' => 'لم يتم التحقق من بريدك الإلكتروني. لن يتم إرسال رسائل لأي من الميزات التالية.', +'noemailprefs' => 'يجب تحديد عنوان بريد إلكتروني لتفعيل هذه الخصائص.', +'emailconfirmlink' => 'أكد عنوان بريدك الإلكتروني', +'invalidemailaddress' => 'لا يمكن قبول عنوان البريد الإلكتروني الذي زودته حيث يبدو خاطئا. من فضلك ضع عنوانا مضبوطا أو أفرغ هذا الحقل.', 'accountcreated' => 'تم إنشاء الحساب', 'accountcreatedtext' => 'تم إنشاء الحساب الخاص ب$1.', +'loginlanguagelabel' => 'اللغة: $1', # Password reset dialog -'resetpass' => 'إعادة ضبط كلمة سر الحساب', +'resetpass' => 'أعد ضبط كلمة سر الحساب', 'resetpass_announce' => 'تم تسجيل دخولك بكلمة سر مؤقتة. للدخول بشكل نهائي، يجب عليك ضبط كلمة سر جديدة هنا:', 'resetpass_text' => '', -'resetpass_header' => 'إعادة ضبط كلمة السر', +'resetpass_header' => 'أعد ضبط كلمة السر', 'resetpass_submit' => 'ضبط كلمة السر والدخول', -'resetpass_success' => 'تم تغيير كلمة السر بنجاخ! يتم تسجيل دخولك الأن...', +'resetpass_success' => 'تم تغيير كلمة السر الخاصة بك بنجاح! يتم تسجيل دخولك الآن...', 'resetpass_bad_temporary' => 'كلمة السر المؤقتة خاطئة. ربما تم تغيير كلمة السر فعليا بنجاح أو ربما قمت بطلب كلمة سر مؤقتة جديدة.', 'resetpass_forbidden' => 'لا يمكن تغيير كلمات السر على هذه الويكي', -'resetpass_missing' => 'لم يتم استقبال بيانات', +'resetpass_missing' => 'لم يتم استقبال بيانات.', # Edit page toolbar 'bold_sample' => 'نص عريض', @@ -618,18 +712,17 @@ $1 ساعة.', 'italic_tip' => 'نص مائل', 'link_sample' => 'عنوان وصلة', 'link_tip' => 'وصلة داخلية', -'extlink_sample' => 'http://www.example.com نص الوصلة', +'extlink_sample' => 'http://www.example.com عنوان الوصلة', 'extlink_tip' => 'وصلة خارجية (تذكر إضافة http:// قبل عنوان الوصلة)', 'headline_sample' => 'نص عنوان رئيسي', 'headline_tip' => 'عنوان من المستوى الثاني', 'math_sample' => 'أدخل الصيغة هنا', -'math_tip' => 'صيغة رياضية', +'math_tip' => 'صيغة رياضية (لا تك)', 'nowiki_sample' => 'أدخل النص غير المنسق هنا', 'nowiki_tip' => 'أهمل تهيئة الويكي', -'image_sample' => 'اسم الصورة|تصغير|تعليق', 'image_tip' => 'إدراج صورة', 'media_tip' => 'وصلة ملف وسائط', -'sig_tip' => 'توقيعك من الساعة والتاريخ', +'sig_tip' => 'توقيعك مع الساعة والتاريخ', 'hr_tip' => 'خط أفقي (تجنب الاستخدام بكثرة)', # Edit pages @@ -642,10 +735,10 @@ $1 ساعة.', 'showpreview' => 'عرض التعديلات', 'showlivepreview' => 'عرض مباشر', 'showdiff' => 'أظهر الفرق', -'anoneditwarning' => "'''تحذير:''' لم تقم بالدخول. سيتم تسجيل عنوان الأيبي الخاص بك في تاريخ هذه الصفحة.", -'missingsummary' => "'''تنبيه:''' لم تقم بكتابة ملخص للتعديل الذي قمت به. إذا قمت باختيار «حفظ» مرة أخرى فسوف يحفظ تغييرك بلا ملخص.", +'anoneditwarning' => "'''تحذير:''' لم تقم بالدخول؛ سيتم تسجيل عنوان الأيبي الخاص بك في تاريخ هذه الصفحة.", +'missingsummary' => "'''تنبيه:''' لم تقم بكتابة ملخص للتعديل الذي قمت به. إذا قمت بضغط حفظ الصفحة مرة أخرى فسوف يتم حفظ تغييرك بلا ملخص.", 'missingcommenttext' => 'الرجاء كتابة تعليق في الأسفل.', -'missingcommentheader' => "'''تنبيه:''' لم تقم بوضع عنوان لهذا التعليق، إذا قمت بالضغط على ''حفظ الصفحة'' مجددا، سيتم حفظ تعليقك بدون عنوان.", +'missingcommentheader' => "'''تنبيه:''' لم تقم بوضع عنوان لهذا التعليق، إذا قمت بالضغط على حفظ الصفحة مجددا، سيتم حفظ تعليقك بدون عنوان.", 'summary-preview' => 'عرض مسبق للملخص', 'subject-preview' => 'معاينة العنوان', 'blockedtitle' => 'المستخدم ممنوع', @@ -653,104 +746,130 @@ $1 ساعة.', سبب المنع هو: ''$2''. وقام بالمنع $1. +انتهاء المنع: $6
    +الممنوع المقصود: $7 + من الممكن التواصل مع $1 للنقاش حول المنع، أو مع أحد [[{{MediaWiki:grouppage-sysop}}|الإداريين]] حول ذلك> تذكر أنه لا يمكن لك استعمال خاصية إرسال رسائل إلكترونية للمستخدمين إلا إذا كنت قد وضعت عنوان بريدي صحيح في صفحة [[Special:Preferences|التفضيلات]] الخاصة بك. -عنوان ال IP الخاص بك حاليا هو $3 وكود المنع هو #$5. يرجى إضافة أيهما أو كلاهما في أي رسالة للتساؤل حول المنع.", -'blockedoriginalsource' => "مصدر '''$1''' ظاهر هنا:", -'blockededitsource' => "مبين بالأسفل نص '''تعديلك''' في '''$1''':", -'whitelistedittitle' => 'الولوج ضروري للتحرير', +عنوان الأيبي الخاص بك حاليا هو $3 وكود المنع هو #$5. يرجى إضافة أيهما أو كلاهما في أي رسالة للتساؤل حول المنع.", +'autoblockedtext' => 'عنوان الأيبي الخاص بك تم منعه تلقائيا لأنه تم استخدامه بواسطة مستخدم آخر، و الذي تم منعه بواسطة $1. +السبب الممنوح هو التالي: + +:\'\'$2\'\' + +انتهاء المنع: $6 + +يمكنك أن تتصل ب $1 أو أحد +[[{{MediaWiki:grouppage-sysop}}|الإداريين]] الآخرين لمناقشة المنع. + +لاحظ أنه لا يمكنك استخدام خاصية "إرسال رسالة لهذا المستخدم" إلا لو كان لديك عنوان بريد إلكتروني صحيح مسجل في [[Special:Preferences|تفضيلاتك]]. + +رقم منعك هو $5. من فضلك اذكر هذا الرقم في أي استفسار.', +'blockedoriginalsource' => "مصدر '''$1''' معروض بالأسفل:", +'blockededitsource' => "مبين بالأسفل نص '''تعديلاتك''' في '''$1''':", +'whitelistedittitle' => 'تسجيل الدخول مطلوب للتحرير', 'whitelistedittext' => 'يجب عليك $1 لتتمكن من تعديل الصفحات.', -'whitelistreadtitle' => 'الولوج ضروري للقراءة', -'whitelistreadtext' => 'يتعين عليك [[Special:Userlogin|الولوج]] لتتمكن من قراءة الصفحات.', -'whitelistacctitle' => 'لا يسمح لك بإنشاء إشتراك', -'whitelistacctext' => 'حتى يُسمح لك بإنشاء حسابات في هذه الويكي عليك أن تقوم [[Special:Userlogin|بالدخول]] وأن تمتلك الصلاحيات المناسبة.', +'whitelistreadtitle' => 'تسجيل الدخول مطلوب للقراءة', +'whitelistreadtext' => 'يتعين عليك [[Special:Userlogin|تسجيل الدخول]] لتتمكن من قراءة الصفحات.', +'whitelistacctitle' => 'لا يسمح لك بإنشاء اشتراك', +'whitelistacctext' => 'حتى يسمح لك بإنشاء حسابات في هذه الويكي عليك أن تقوم [[Special:Userlogin|بالدخول]] وأن تمتلك الصلاحيات المناسبة.', 'confirmedittitle' => 'تأكيد البريد الإلكتروني مطلوب لبدء التعديل', 'confirmedittext' => 'يجب عليك تأكيد بريدك الإلكتروني قبل التعديل. من فضلك اكتب و أكد بريدك الإلكتروني من خلال [[Special:Preferences|تفضيلاتك]]', 'nosuchsectiontitle' => 'لا يوجد قسم بهذا الاسم', -'nosuchsectiontext' => 'لقد حاولت تحرير قسما غير موجودا. وحيث أنه لا يوجد القسم $1، فلا يمكن حفظ تعديلك.', -'loginreqtitle' => 'مطلوب الدخول أولا.', +'nosuchsectiontext' => 'لقد حاولت تحرير قسم غير موجود. وحيث أنه لا يوجد القسم $1، فلا يوجد مكان لحفظ تعديلك.', +'loginreqtitle' => 'تسجيل الدخول مطلوب', 'loginreqlink' => 'دخول', 'loginreqpagetext' => 'يجب عليك $1 لتشاهد صفحات أخرى.', 'accmailtitle' => 'تم إرسال كلمة السر.', 'accmailtext' => "تم إرسال كلمة السر الخاصة بـ '$1' إلى العنوان $2.", 'newarticle' => '(جديد)', 'newarticletext' => "لقد تبعت وصلة لصفحة لم يتم إنشائها بعد. -لإنشاء هذه الصفحة إبدأ بالكتابة في الصندوق بالأسفل. -(أنظر في [[{{MediaWiki:helppage}}|صفحة المساعدة]] للمزيد من المعلومات) -إذا كانت زيارتك لهذه الصفحة بالخطأ، إضغم على زر ''رجوع'' في متصفح الإنترنت لديك.", +لإنشاء هذه الصفحة ابدأ الكتابة في الصندوق بالأسفل. +(انظر في [[{{MediaWiki:helppage}}|صفحة المساعدة]] للمزيد من المعلومات) +إذا كانت زيارتك لهذه الصفحة بالخطأ، اضغط على زر ''رجوع'' في متصفح الإنترنت لديك.", 'anontalkpagetext' => '---- هذه صفحة نقاش لمستخدم مجهول، وهو المستخدم الذي لم يقم بإنشاء حساب في {{SITENAME}}، أو لا يستعمل ذلك الحساب. -لذا يتم استعمال رقم ال IP للتعريف به. من الممكن أن يشترك عدد من المستخدمين بنفس رقم ال IP. إذا كنت مستخدم مجهول +لذا يتم استعمال رقم الأيبي للتعريف به. من الممكن أن يشترك عدد من المستخدمين بنفس رقم الأيبي. إذا كنت مستخدما مجهولا وترى أن رسائل خير موجهة لك قد وصلتك، من الممكن أن تقوم [[Special:Userlogin|بإنشاء حساب أو القيام بالدخول]] حتى يزول الخلط بينك وبين المستخدمين المجهولين الآخرين.', -'noarticletext' => '(لا يوجد حاليا أي نص في هذه الصفحة)', -'clearyourcache' => "ملاحظة: قم بإعادة تحميل الصفحة لرؤية التغييرات. إذا لم تظهر التغييرات بعد إعادة التحميل، قد يكون متصفح الإنترنت الذي تستخدمه يقوم بعرض نسخة مخزنة سابقًا. لتجنب عرض هذه النسخة، قد يلزم أن تقوم بأحد هذه الخطوات: +'noarticletext' => 'لا يوجد حاليا أي نص في هذه الصفحة، يمكنك [[Special:Search/{{PAGENAME}}|البحث عن عنوان هذه الصفحة]] في الصفحات الأخرى or [{{fullurl:{{FULLPAGENAME}}|action=edit}} تعديل هذه الصفحة].', +'clearyourcache' => "ملاحظة: قم بإعادة تحميل الصفحة لرؤية التغييرات. إذا لم تظهر التغييرات بعد إعادة التحميل، قد يكون متصفح الإنترنت الذي تستخدمه يقوم بعرض نسخة مخزنة سابقا. لتجنب عرض هذه النسخة، قد يلزم أن تقوم بأحد هذه الخطوات: * في متصفح '''موزيلا''' أو '''فايرفوكس''' أو '''سفاري''' اضغظ على ''مفتاح Shift'' أثناء قيامك بضغط زر إعادة التحميل (''Refresh'') أو اضغط ''Ctrl-Shift-R'' (''Cmd-Shift-R'' في حاسب أبل). * في متصفح '''إنترنت إكسبلورر''' اضغط على مفتاح ''Ctrl'' أثناء قيامك بالنقر على زر إعادة التحميل (''Refresh'') أو اضغط ''Ctrl-F5''. * في متصفح''' كونكيرر''' (Konqueror) انقر على زر إعادة التحميل (''Reload'') أو اضغط على مفتاح '''F5'''. -* في متصفح '''أوبرا''' (Opera) قد يكون من الضروري مسح المحتويات المخزنة عن طريق قائمة التفضيلات (''Tools→Preferences'').", -'usercssjsyoucanpreview' => "ملاحظة: استعمل زرّ 'عرض التعديلات' لتجربة النمط (CSS) أو الجافا سكريبت الجديد قبل حفظ الصفحة.", +* في متصفح '''أوبرا''' (Opera) قد يكون من الضروري مسح المحتويات المخزنة عن طريق قائمة التفضيلات (''Tools→Preferences'').", +'usercssjsyoucanpreview' => "ملاحظة: استعمل زر 'عرض التعديلات' لتجربة النمط (CSS) أو الجافا سكريبت الجديد قبل حفظ الصفحة.", 'usercsspreview' => "'''تذكر أنك تقوم بعرض الأنماط المتراصة (CSS) فقط وأنك لم تقم بحفظها بعد!'''", -'userjspreview' => "'''تذكر أنك فقط تجرب/تعرض الجافا سكريبت الخاص بك, لم يتم الحفظ بعد!'''", -'userinvalidcssjstitle' => "'''تحذير:''' لا توجد واجهة \"\$1\". تذكر أن ملفات ال.css و ال.js تستخدم حروف صغيرة في العنوان , كمثال User:Foo/monobook.css و ليس User:Foo/Monobook.css.", +'userjspreview' => "'''تذكر أنك فقط تجرب/تعرض الجافا سكريبت الخاص بك، لم يتم الحفظ بعد!'''", +'userinvalidcssjstitle' => "'''تحذير:''' لا توجد واجهة \"\$1\". تذكر أن ملفات ال.css و ال.js تستخدم حروف صغيرة في العنوان ، كمثال User:Foo/monobook.css و ليس User:Foo/Monobook.css.", 'updated' => '(محدثة)', 'note' => 'ملاحظة:', -'previewnote' => 'تذكر، هذا فقط عرض مسبق للصفحة، ولم يتم حفظه بعد!', +'previewnote' => 'تذكر، هذا فقط عرض مسبق للصفحة، ولم يتم حفظه بعد!', 'previewconflict' => 'العرض يوضح النص الموجود في صندوق التحرير العلوي و الذي سيظهر إذا اخترت الحفظ.', -'session_fail_preview' => 'عفوًا! لم نتمكن من حفظ التعديلات التي قمت بها نتيجة لضياع في المعلومات في هذه الجلسة. الرجاء المحاولة مرة أخرى. في حال استمرار المشكلة حاول أن تقوم بالخروج ومن ثم الدخول مرة أخرى.', -'session_fail_preview_html' => "عذرًا ! لا نستطيع تسجيل تعديلاتك بسبب فقدان معلومات حول الجلسات. +'session_fail_preview' => 'عفوا! لم نتمكن من حفظ التعديلات التي قمت بها نتيجة لضياع بيانات هذه الجلسة. الرجاء المحاولة مرة أخرى. في حال استمرار المشكلة حاول أن تقوم بالخروج ومن ثم الدخول مرة أخرى.', +'session_fail_preview_html' => "عذرا ! لا نستطيع تسجيل تعديلاتك بسبب فقدان بيانات الجلسة. -''بسبب أن wiki فعل HTML , تم إخفاء عرض التعديلات كوقاية و حماية ضد الهجمات ب Javascript.'' +''بسبب أن wiki فعل HTML ، تم إخفاء عرض التعديلات كوقاية و حماية ضد الهجمات ب Javascript.'' -إدا كانت هذه محاولة تعديل صادقة, حاول مرة أخرى. و في حالة الفشل حاول الخروج تم الدخول مرة أخرى.", -'importing' => 'جاري استيراد $1', +إدا كانت هذه محاولة تعديل صادقة، حاول مرة أخرى؛ و في حالة الفشل حاول الخروج تم الدخول مرة أخرى.", +'token_suffix_mismatch' => 'تعديلك تم رفضه لأن عميلك أخطأ في علامات الترقيم +في نص التعديل. التعديل تم رفضه لمنع فساد نص المقالة. +هذا يحدث أحيانا عندما تستخدم خدمة بروكسي مجهولة أساسها الويب معيبة.', 'editing' => 'تحرير $1', -'editinguser' => 'تحرير $1', +'editinguser' => 'تحرير مستخدم $1', 'editingsection' => 'تحرير $1 (قسم)', 'editingcomment' => 'تحرير $1 (تعليق)', 'editconflict' => 'تضارب في التحرير: $1', -'explainconflict' => 'لقد قام أحد ما بتعديل الصفحة بعد أن بدأت انت بتحريرها. +'explainconflict' => 'لقد قام أحد ما بتعديل الصفحة بعد أن بدأت أنت بتحريرها. صندوق النصوص العلوي يحتوي على النص الموجود حاليا في الصفحة. والتغييرات التي قمت أنت بها موجودة في الصندوق في أسفل الصفحة. يجب أن تقوم بدمج تغييراتك في النص الموجود حاليا. -فقط ما هو موجود في الصندوق العلوي هو ما سيتم حفظه وإستعاله عند الضغط على زر "حفظ الصفحة". -

    ', +فقط ما هو موجود في الصندوق العلوي هو ما سيتم حفظه عند الضغط على زر "حفظ الصفحة". +
    ', 'yourtext' => 'النص الذي كتبته', 'storedversion' => 'النسخة المخزنة', -'nonunicodebrowser' => 'تحذير: متصفحك لا يوافق الترميز الموحد. تمت معالجة هذا لكي تتمكن من تحرير الصفحات بأمان: حروف ليست من ASCII سوف تظهر في صندوق التحريرx كأكواد سداسي عشرية.', -'editingold' => ' تحذير: أنت تقوم الآن بتحرير نسخة قديمة من هذه الصفحة. إذا قمت بحفظها، سيتم فقدات كافة التغييرات التي حدثت بعد هذه النسخة. ', +'nonunicodebrowser' => 'تحذير: متصفحك لا يوافق الترميز الموحد. تمت معالجة هذا لكي تتمكن من تحرير الصفحات بأمان: الحروف التي ليست ASCII سوف تظهر في صندوق التحرير كأكواد سداسي عشرية.', +'editingold' => ' تحذير: أنت تقوم الآن بتحرير نسخة قديمة من هذه الصفحة. +إذا قمت بحفظها، ستفقد كافة التغييرات التي حدثت بعد هذه النسخة. ', 'yourdiff' => 'الفروقات', -'copyrightwarning' => 'يرجى الملاحظة أن جميع المساهمات هنا خاضعة وصادرة تحت ترخيص $2 (انظر في $1 للمزيد من التفاصيل) +'copyrightwarning' => 'يرجى الملاحظة أن جميع المساهمات ل {{SITENAME}} خاضعة وصادرة تحت ترخيص $2 (انظر في $1 للمزيد من التفاصيل) إذا لم ترد أن تخضع كتابتك للتعديل والتوزيع الحر، لا تضعها هنا
    . كما أنك تتعهد بأنك قمت بكتابة ما هو موجود بنفسك، أو قمت بنسخها من مصدر يخضع ضمن الملكية العامة، أو مصدر حر آخر. لا ترسل أي عمل ذي حقوق محفوظة بدون الإذن من صاحب الحق.', -'copyrightwarning2' => 'من فضلك لاحظ أن حميع المشاركات في {{SITENAME}} يمكن أن تعدل أو تتغير أو تحذف من قبل المشاركين الآخرين. إذا لم تكن ترغب أن تعدل مشاركاتك بهذا الشكل, لا تضعها هنا.
    -انت أيضا تقر أنك كتبت هذا بنفسك, أو نسختها من مصدر لا يخضع لحقوق النشر أو يسمح بالنقل بشكلا ما (أنظر $1 للتفاصيل). +'copyrightwarning2' => 'من فضلك لاحظ أن حميع المشاركات في {{SITENAME}} يمكن أن تعدل أو تتغير أو تحذف من قبل المشاركين الآخرين. إذا لم تكن ترغب أن تعدل مشاركاتك بهذا الشكل، لا تضعها هنا.
    +انت أيضا تقر أنك كتبت هذا بنفسك، أو نسختها من مصدر لا يخضع لحقوق النشر أو يسمح بالنقل بشكلا ما (انظر $1 للتفاصيل). لا تضف أي نص ذي حقوق محفوظة!', -'longpagewarning' => 'تنبيه: هذه صفحة طويلة وحجمها $1 كيلوبايت، بعض متصفحات الشبكة قد تواجه مشاكل عند محاولة تحرير صفحات يزيد حجمها عن 32 كيلوبايت. يرجى تقسيم الصفحة إلى أقسام أصغر حجمًا إن أمكن.', +'longpagewarning' => 'تنبيه: هذه صفحة طويلة وحجمها $1 كيلوبايت، بعض متصفحات الشبكة قد تواجه مشاكل عند محاولة تحرير صفحات يزيد حجمها عن 32 كيلوبايت. يرجى تقسيم الصفحة إلى أقسام أصغر حجما إن أمكن.', 'longpageerror' => 'خطأ: النص الذي أدخلته يجاوز $1 كيلوبايت ، و هذا أكبر من الحد الأقصى و هو $2 كيلوبايت. لا يمكن حفظ الصفحة.', 'readonlywarning' => 'تحذير: لقد أغلقت قاعدة البيانات للصيانة، لذلك لن تتمكن من حفظ التعديلات التي قمت بها حاليا. إذا رغبت بإمكانك أن تنسخ النص الذي تعمل عليه وتحفظه في ملف نصي إلى وقت لاحق.', -'protectedpagewarning' => 'تحذير: هذه الصفحة محمية. فقط المستخدمون ذوو الصلاحيات الإدارية يمكن أن يقوموا بتحريرها. تأكد من اتباع [[Project:تعليمات الصفحات المحمية|التعليمات الخاصة بالصفحات المحمية]] قبل التعديل.', -'semiprotectedpagewarning' => "'''ملاحظة:''' هذه الصفحة محمية بحيث يتم تعديلها فقط من قبل المستخدمين المسجلين بالموسوعة.", -'cascadeprotectedwarning' => 'تحذير: تمت حماية هذه الصفحة بحيث يستطيع المستخدمون ذوو الصلاحيات الإدارية فقط تعديلها، وذلك لكونها مدمجة في الصفحات التالية والتي تمت حمايتها بخاصية "حماية الصفحات المدمجة":', +'protectedpagewarning' => 'تحذير: هذه الصفحة محمية. فقط المستخدمون ذوو الصلاحيات الإدارية يمكن أن يقوموا بتحريرها.', +'semiprotectedpagewarning' => "'''ملاحظة:''' هذه الصفحة محمية بحيث يتم تعديلها فقط من قبل المستخدمين المسجلين.", +'cascadeprotectedwarning' => 'تحذير: تمت حماية هذه الصفحة بحيث يستطيع المستخدمون ذوو الصلاحيات الإدارية فقط تعديلها، وذلك لكونها مدمجة في {{PLURAL:$1|الصفحة|الصفحات}} التالية والتي تمت حمايتها بخاصية "حماية الصفحات المدمجة":', 'templatesused' => 'القوالب المستخدمة في هذه الصفحة:', 'templatesusedpreview' => 'القوالب المستخدمة في هذا العرض المسبق:', 'templatesusedsection' => 'القوالب المستخدمة في هذا القسم:', 'template-protected' => '(حماية كاملة)', 'template-semiprotected' => '(حماية جزئية)', -'edittools' => '', +'edittools' => '', 'nocreatetitle' => 'تم تحديد إنشاء الصفحات', 'nocreatetext' => 'قام الموقع بتحديد القدرة على إنشاء صفحات جديدة. -يمكنك العودة و تحرير صفحة موجودة بالفعل, أو [[Special:Userlogin|الدخول أو تسجيل حساب]].', +يمكنك العودة و تحرير صفحة موجودة بالفعل، أو [[Special:Userlogin|الدخول أو تسجيل حساب]].', +'nocreate-loggedin' => 'لا تمتلك الصلاحية لإنشاء صفحات جديدة على هذه الويكي.', +'permissionserrors' => 'أخطاء الصلاحيات', +'permissionserrorstext' => 'لا تمتلك الصلاحية لفعل هذا، {{PLURAL:$1|للسبب التالي|للأسباب التالية}}:', +'recreate-deleted-warn' => "'''تحذير: أنت تقوم بإعادة إنشاء صفحة تم حذفها في السابق.''' + +يجب عليك التيقن من أن الاستمرار بتحرير هذه الصفحة ملائم. +سجل الحذف لهذه الصفحة معروض هنا:", # "Undo" feature 'undo-success' => 'تم استرجاع التعديل. من فضلك، أكد التعديل وقم بحفظ الصفحة أدناه.', 'undo-failure' => 'لم ينجح الاسترجاع بسبب تعديلات متعارضة تمت على الصفحة.', -'undo-summary' => 'الرجوع عن التعديل $1 بواسطة [[Special:Contributions/$2|$2]] ([[نقاش المستخدم:$2|نقاش]])', +'undo-summary' => 'الرجوع عن التعديل $1 بواسطة [[Special:Contributions/$2|$2]] ([[User talk:$2|نقاش]])', # Account creation failure 'cantcreateaccounttitle' => 'لم يتم إنشاء الحساب.', -'cantcreateaccounttext' => 'تم منع إنشاء حساب خاص من عنوانك هذا ($1). +'cantcreateaccounttext' => 'تم منع إنشاء الحسابات من عنوان الأيبي هذا ($1). ربما يكون هذا بسبب التخريب الصادر من مدرستك أو مزود الإنترنت الخاص بك.', # History pages @@ -763,9 +882,9 @@ $1 ساعة.', 'loadhist' => 'تحميل تاريخ الصفحة', 'currentrev' => 'النسخة الحالية', 'revisionasof' => 'نسخة $1', -'revision-info' => 'تعديل $1 بواسطة $2', -'previousrevision' => '→ نسخة أقدم', -'nextrevision' => 'نسخة أحدث ←', +'revision-info' => 'نسخة $1 بواسطة $2', +'previousrevision' => '←نسخة أقدم', +'nextrevision' => 'نسخة أحدث→', 'currentrevisionlink' => 'رؤية النسخة الحالية', 'cur' => 'الحالي', 'next' => 'التالي', @@ -773,8 +892,9 @@ $1 ساعة.', 'orig' => 'الأصلي', 'page_first' => 'الأولى', 'page_last' => 'الأخيرة', -'histlegend' => 'مفتاح: (الحالي) = الفرق مع النسخة الحالية -(السابق) = الفروقات مع النسخة السابقة، ط = تغيير طفيف', +'histlegend' => 'اختيار الفرق: علم على صناديق النسخ للمقارنة و اضغط قارن بين النسخ المختارة أو الزر بالأسفل.
    +مفتاح: (الحالي) = الفرق مع النسخة الحالية +(السابق) = الفرق مع النسخة السابقة، ط = تغيير طفيف', 'deletedrev' => '[محذوف]', 'histfirst' => 'أول', 'histlast' => 'آخر', @@ -783,13 +903,13 @@ $1 ساعة.', # Revision feed 'history-feed-title' => 'تاريخ التعديل', -'history-feed-description' => 'تاريخ التعديل على هذه الصفحة في الويكي', +'history-feed-description' => 'تاريخ التعديل لهذه الصفحة في الويكي', 'history-feed-item-nocomment' => '$1 في $2', # user at time 'history-feed-empty' => 'الصفحة المطلوبة غير موجودة. من المحتمل أن تكون هذه الصفحة قد حذفت أو نقلت. حاول [[Special:Search|البحث في الويكي]] عن صفحات جديدة ذات صلة.', # Revision deletion -'rev-deleted-comment' => '(تم حذف تعليق)', -'rev-deleted-user' => '(تم حذف اسم مستخدم)', +'rev-deleted-comment' => '(تم مسح تعليق)', +'rev-deleted-user' => '(تم مسح اسم مستخدم)', 'rev-deleted-event' => '(تم مسح المدخلة)', 'rev-deleted-text-permission' => '

    cells collapse their height to a single line. + +Known to be fixed in 1.2.1 (Gecko 20021130), but the CSS hacks I've tried +with overflow-x disable the scrolling all the way until Mozilla 1.8 / FF 1.5 +and break Opera as well. + +So... we check for reaaaally old Gecko and hack in an alternate rule to let +the wide cells spill instead of scrolling them. Not ideal as it won't work +if JS is disabled, of course. +*/ + +if (navigator && navigator.product == "Gecko" && navigator.productSub < "20021130") { + var sheets = document.styleSheets; + var lastSheet = sheets[sheets.length-1]; + lastSheet.insertRule( + "table.diff td div { overflow: visible; }", + lastSheet.cssRules.length); +} \ No newline at end of file diff --git a/skins/common/images/spinner.gif b/skins/common/images/spinner.gif new file mode 100644 index 00000000..57d749b0 Binary files /dev/null and b/skins/common/images/spinner.gif differ diff --git a/skins/common/oldshared.css b/skins/common/oldshared.css new file mode 100644 index 00000000..27d93a03 --- /dev/null +++ b/skins/common/oldshared.css @@ -0,0 +1,465 @@ +/* + * oldshared.css + * This file contains CSS settings common to Wikistandard, Nostalgia and + * CologneBlue, the old pre-Monobook skins + */ + +/* For clarity, explicitly state some recommendations from to make sure the editsection links scale right */ + +h1 { font-size: 2em; } +h2 { font-size: 1.5em; } +h3 { font-size: 1.17em; } +h5 { font-size: .83em; } +h6 { font-size: .75em; } +h1, h2, h3, h4, h5, h6 { font-weight: bolder } + +/* Now the custom parts */ + +/* Make edit sections (which are inside h# tags) normal-sized */ +.editsection { + font-weight: normal; +} +h1 .editsection { font-size: 50% } +h2 .editsection { font-size: 66.7% } +h3 .editsection { font-size: 85.5% } +h5 .editsection { font-size: 120% } +h6 .editsection { font-size: 133% } + +#footer { clear: both } +/* images */ +div.floatright { float: right; clear: right; margin: 0 0 1em 1em; } +div.floatright p { font-style: italic; } +div.floatleft { float: left; clear: left; margin: 0.3em 0.5em 0.5em 0; } +div.floatleft p { font-style: italic; } + + +/* Print-specific things to hide */ +.printfooter { + display: none; +} + +/* table standards */ +table.rimage { + float:right; + margin-left:1em; + margin-bottom:1em; + text-align:center; + font-size:smaller; +} + +/* thumbnails */ +div.thumb { + margin-bottom: .5em; + border-style: solid; + border-color: white; + width: auto; +} +div.thumbinner { + border: 1px solid #ccc; + padding: 3px !important; + background-color: #f9f9f9; + font-size: 94%; + text-align: center; + overflow: hidden; +} +html .thumbimage { + border: 1px solid #ccc; +} +html .thumbcaption { + border: none; + text-align: left; + line-height: 1.4em; + padding: 3px !important; + font-size: 94%; +} +div.magnify { + float: right; + border: none !important; + background: none !important; +} +div.magnify a, div.magnify img { + display: block; + border: none !important; + background: none !important; +} +div.tright { + clear: right; + float: right; + border-width: .5em 0 .8em 1.4em; +} +div.tleft { + float: left; + clear: left; + margin-right: .5em; + border-width: .5em 1.4em .8em 0; +} +img.thumbborder { + border: 1px solid #dddddd; +} + +/* Page history styling */ +/* the auto-generated edit comments */ +.autocomment { color: #4b4b4b; } +#pagehistory span.user { + margin-left: 1.4em; + margin-right: 0.4em; +} +#pagehistory span.minor { font-weight: bold; } +#pagehistory li { border: 1px solid White; } +#pagehistory li.selected { + background-color:#f9f9f9; + border:1px dashed #aaaaaa; +} + +img { border: none; } + +#toc, +.toc { + border: 1px solid #bba; + background-color: #f7f8ff; + padding: 5px; + font-size: 95%; + text-align: center; +} +#toc h2, +.toc h2 { + display: inline; + border: none; + padding: 0; + font-size: 100%; + font-weight: bold; +} +#toc ul, +.toc ul { + list-style-type: none; + list-style-image: none; + margin-left: 0; + padding-left: 0; + text-align: left; +} +#toc ul ul, +.toc ul ul { + margin: 0 0 0 2em; +} +#toc .toctoggle, +.toc .toctoggle { + font-size: 94%; +} + + +.error { + color: red; + font-size: larger; +} + +/* preference page with js-genrated toc */ +#preftoc { + float: left; + margin: 1em 1em 1em 1em; + width: 13em; +} +#preftoc li { border: 1px solid White; } +#preftoc li.selected { + background-color:#f9f9f9; + border:1px dashed #aaaaaa; +} +#preftoc a, +#preftoc a:active { + display: block; + color: #005189; +} +#prefcontrol { + clear: left; + float: left; + margin-top: 1em; +} +div.prefsectiontip { + font-size: 94%; + margin-top: 0.4em; + color: #666; +} +fieldset.prefsection { margin-top: 1em } +fieldset.operaprefsection { margin-left: 15em } + +/* emulate center */ +.center { + width: 100%; + text-align: center; +} +*.center * { + margin-left: auto; + margin-right: auto; +} +/* small for tables and similar */ +.small, .small * { font-size: 94%; } +table.small { font-size: 100% } + +div.townBox { + position:relative; + float:right; + background:White; + margin-left:1em; + border: 1px solid gray; + padding:0.3em; + width: 200px; + overflow: hidden; + clear: right; +} +div.townBox dl { + padding: 0; + margin: 0 0 0.3em 0; + font-size: 96%; +} +div.townBox dl dt { + background: none; + margin: 0.4em 0 0 0; +} +div.townBox dl dd { + margin: 0.1em 0 0 1.1em; + background-color: #f3f3f3; +} +/* use this instead of #toc for page content */ +.toccolours { + border:1px solid #aaaaaa; + background-color:#f9f9f9; + padding:5px; + font-size: 95%; +} +#siteNotice { + border:1px solid #aaaaaa; + padding-left: 0.5em; + padding-right: 0.5em; +} +.redirectText { + font-size:150%; + margin:5px; +} +.searchmatch { + color: red; + font-weight: bold; +} +.sharedUploadNotice { + font-style: italic; +} +span.unpatrolled { + font-weight:bold; + color:red; +} + +span.updatedmarker { + color:black; + background-color:#00FF00; +} + +table.gallery { + border: 1px solid #cccccc; + margin: 2px; + padding: 2px; + background-color:#ffffff; +} + +table.gallery tr { + vertical-align:top; +} + +table.gallery td { + vertical-align:top; + background-color:#f9f9f9; + border: solid 2px white; +} + +div.gallerybox { + margin: 2px; + width: 150px; +} + +div.gallerybox div.thumb { + text-align: center; + border: 1px solid #cccccc; + margin: 2px; +} + +div.gallerytext { + overflow: hidden; + font-size: 94%; + padding: 2px 4px; +} + +span.comment { + font-style: italic; +} + +span.changedby { + font-size: 95%; +} + +.previewnote { + text-align: center; + color: #cc0000; +} +.editExternally { + border-style:solid; + border-width:1px; + border-color:gray; + background: #ffffff; + padding:3px; + margin-top:0.5em; + float:left; + font-size:small; + text-align:center; +} +.editExternallyHelp { + font-style:italic; + color:gray; +} + +li span.deleted { + text-decoration: line-through; + color: #888; + font-style: italic; +} + +/* Classes for EXIF data display */ +table.mw_metadata { + margin-left: 0.5em; +} + +table.mw_metadata caption { font-weight: bold; } +table.mw_metadata th { font-weight: normal; } +table.mw_metadata td { padding: 0.1em; } + +table.mw_metadata { + border: none; + border-collapse: collapse; +} +table.mw_metadata td, table.mw_metadata th { + border: 1px solid #aaaaaa; + padding-left: 4px; + padding-right: 4px; +} +table.mw_metadata th { + background-color: #f9f9f9; +} +table.mw_metadata td { + background-color: #fcfcfc; +} +table.mw_metadata td.spacer { + background: inherit; + border-top: none; + border-bottom: none; +} +table.collapsed tr.collapsable { + display: none; +} + +.visualClear { + clear: both; +} + +#mw_trackbacks { + border: solid 1px #bbbbff; + background-color: #eeeeff; + padding: 0.2em; +} + +/* Allmessages table */ + +#allmessagestable th { + background-color: #b2b2ff; +} + +#allmessagestable tr.orig { + background-color: #ffe2e2; +} + +#allmessagestable tr.new { + background-color: #e2ffe2; +} + +#allmessagestable tr.def { + background-color: #f0f0ff; +} + +#jump-to-nav { + display: none; +} + +/* Keep this temporarily so that cached pages will display right */ +table.gallery td.galleryheader { + text-align: center; + font-weight: bold; +} +table.gallery caption { + font-weight: bold; +} + +div.multipageimagenavbox { + border: solid 1px silver; + padding: 4px; + margin: 1em; + -moz-border-radius: 6px; + background: #f0f0f0; +} + +div.multipageimagenavbox div.thumb { + border: none; + margin-left: 2em; + margin-right: 2em; +} + +div.multipageimagenavbox hr { + margin: 6px; +} + +table.multipageimage td { + text-align: center; +} + +/* + Table pager (e.g. Special:Imagelist) + - remove underlines from the navigation link + - collapse borders + - set the borders to outsets (similar to Special:Allmessages) + - remove line wrapping for all td and th, set background color + - restore line wrapping for the last two table cells (description and size) +*/ +.TablePager_nav a { text-decoration: none; } +.TablePager { border-collapse: collapse; } +.TablePager, .TablePager td, .TablePager th { + border: 0.15em solid #777777; + padding: 0 0.15em 0 0.15em; +} +.TablePager th { background-color: #eeeeff } +.TablePager td { background-color: #ffffff } +.TablePager tr:hover td { background-color: #eeeeff } + +.imagelist td, .imagelist th { white-space: nowrap } +.imagelist .TablePager_col_links { background-color: #eeeeff } +.imagelist .TablePager_col_img_description { white-space: normal } +.imagelist th.TablePager_sort { background-color: #ccccff } + +.templatesUsed { margin-top: 1em; } + +/* Convenience links on Special:Ipblocklist */ +p.mw-ipb-conveniencelinks { + font-size: 90%; + float: right; +} + +/* Recreating-deleted-page warning and log entries */ +div#mw-recreate-deleted-warn ul li { + font-size: 95%; +} + + +.MediaTransformError { + border: thin solid #777; + background-color: #ccc; + padding: 0.1em; +} +.MediaTransformError td { + text-align: center; + vertical-align: middle; + font-size: 90%; +} diff --git a/skins/common/protect.js b/skins/common/protect.js index d8d1aa1c..b3eec3bd 100644 --- a/skins/common/protect.js +++ b/skins/common/protect.js @@ -1,89 +1,94 @@ -function protectInitialize(tableId, labelText) { - if (document.createTextNode) { - var box = document.getElementById(tableId); - if (!box) - return false; - - var tbody = box.getElementsByTagName('tbody')[0]; - var row = document.createElement('tr'); - tbody.appendChild(row); - - row.appendChild(document.createElement('td')); - var col2 = document.createElement('td'); - row.appendChild(col2); - - var check = document.createElement('input'); - check.id = "mwProtectUnchained"; - check.type = "checkbox"; - check.onclick = protectChainUpdate; - col2.appendChild(check); - - var space = document.createTextNode(" "); - col2.appendChild(space); - - var label = document.createElement('label'); - label.setAttribute("for", "mwProtectUnchained"); - label.appendChild(document.createTextNode(labelText)); - col2.appendChild(label); - - if (protectAllMatch()) { - check.checked = false; - protectEnable(false); - } else { - check.checked = true; - protectEnable(true); - } +/** + * Set up the protection chaining interface (i.e. "unlock move permissions" checkbox) + * on the protection form + * + * @param String tableId Identifier of the table containing UI bits + * @param String labelText Text to use for the checkbox label + */ +function protectInitialize( tableId, labelText ) { + if( !( document.createTextNode && document.getElementById && document.getElementsByTagName ) ) + return false; - allowCascade(); - - return true; - } - return false; + var box = document.getElementById( tableId ); + if( !box ) + return false; + + var tbody = box.getElementsByTagName( 'tbody' )[0]; + var row = document.createElement( 'tr' ); + tbody.appendChild( row ); + + row.appendChild( document.createElement( 'td' ) ); + var col = document.createElement( 'td' ); + row.appendChild( col ); + + var check = document.createElement( 'input' ); + check.id = 'mwProtectUnchained'; + check.type = 'checkbox'; + col.appendChild( check ); + addClickHandler( check, protectChainUpdate ); + + col.appendChild( document.createTextNode( ' ' ) ); + var label = document.createElement( 'label' ); + label.setAttribute( 'for', 'mwProtectUnchained' ); + label.appendChild( document.createTextNode( labelText ) ); + col.appendChild( label ); + + check.checked = !protectAllMatch(); + protectEnable( check.checked ); + + allowCascade(); + + return true; } function allowCascade() { - var pr_types = document.getElementsByTagName("select"); - for (var i = 0; i < pr_types.length; i++) { - if (pr_types[i].id.match(/^mwProtect-level-/)) { - var selected_level = pr_types[i].getElementsByTagName("option")[pr_types[i].selectedIndex].value; - if ( !isCascadeableLevel(selected_level) ) { - document.getElementById('mwProtect-cascade').checked=false; - document.getElementById('mwProtect-cascade').disabled=true; + var lists = protectSelectors(); + for( var i = 0; i < lists.length; i++ ) { + if( lists[i].selectedIndex > -1 ) { + var items = lists[i].getElementsByTagName( 'option' ); + var selected = items[ lists[i].selectedIndex ].value; + if( wgCascadeableLevels.indexOf( selected ) == -1 ) { + document.getElementById( 'mwProtect-cascade' ).checked = false; + document.getElementById( 'mwProtect-cascade' ).disabled = true; return false; } } } - document.getElementById('mwProtect-cascade').disabled=false; + document.getElementById( 'mwProtect-cascade' ).disabled = false; return true; } -function isCascadeableLevel( level ) { - for (var k = 0; k < wgCascadeableLevels.length; k++) { - if ( wgCascadeableLevels[k] == level ) { - return true; - } - } - return false; -} - +/** + * When protection levels are locked together, update the rest + * when one action's level changes + * + * @param Element source Level selector that changed + */ function protectLevelsUpdate(source) { - if (!protectUnchained()) { - protectUpdateAll(source.selectedIndex); - } + if( !protectUnchained() ) + protectUpdateAll( source.selectedIndex ); allowCascade(); } +/** + * Update chain status and enable/disable various bits of the UI + * when the user changes the "unlock move permissions" checkbox + */ function protectChainUpdate() { - if (protectUnchained()) { - protectEnable(true); + if( protectUnchained() ) { + protectEnable( true ); } else { protectChain(); - protectEnable(false); + protectEnable( false ); } allowCascade(); } - +/** + * Are all actions protected at the same level? + * + * @return boolean + */ function protectAllMatch() { var values = new Array(); protectForSelectors(function(set) { @@ -97,17 +102,22 @@ function protectAllMatch() { return true; } +/** + * Is protection chaining on or off? + * + * @return bool + */ function protectUnchained() { - var unchain = document.getElementById("mwProtectUnchained"); - if (!unchain) { - alert("This shouldn't happen"); - return false; - } - return unchain.checked; + var unchain = document.getElementById( 'mwProtectUnchained' ); + return unchain + ? unchain.checked + : true; // No control, so we need to let the user set both levels } +/** + * Find the highest-protected action and set all others to that level + */ function protectChain() { - // Find the highest-protected action and bump them all to this level var maxIndex = -1; protectForSelectors(function(set) { if (set.selectedIndex > maxIndex) { @@ -117,6 +127,11 @@ function protectChain() { protectUpdateAll(maxIndex); } +/** + * Protect all actions at the specified level + * + * @param int index Protection level + */ function protectUpdateAll(index) { protectForSelectors(function(set) { if (set.selectedIndex != index) { @@ -125,6 +140,11 @@ function protectUpdateAll(index) { }); } +/** + * Apply a callback to each protection selector + * + * @param callable func Callback function + */ function protectForSelectors(func) { var selectors = protectSelectors(); for (var i = 0; i < selectors.length; i++) { @@ -132,6 +152,11 @@ function protectForSelectors(func) { } } +/** + * Get a list of all protection selectors on the page + * + * @return Array + */ function protectSelectors() { var all = document.getElementsByTagName("select"); var ours = new Array(); @@ -144,6 +169,11 @@ function protectSelectors() { return ours; } +/** + * Enable/disable protection selectors + * + * @param boolean val Enable? + */ function protectEnable(val) { // fixme var first = true; diff --git a/skins/common/shared.css b/skins/common/shared.css new file mode 100644 index 00000000..f6d63ab8 --- /dev/null +++ b/skins/common/shared.css @@ -0,0 +1,48 @@ +/** + * CSS in this file is used by *all* skins (that have any CSS at all). Be + * careful what you put in here, since what looks good in one skin may not in + * another, but don't ignore the poor non-Monobook users either. + */ +.mw-plusminus-null { color: #aaa; } + +.texvc { direction: ltr; unicode-bidi: embed; } +img.tex { vertical-align: middle; } +span.texhtml { font-family: serif; } + +/* Stop floats from intruding into edit area in previews */ +#toolbar, #wpTextbox1 { clear: both; } + +div#mw-js-message { + margin: 1em 5%; + padding: 0.5em 2.5%; + border: solid 1px #ddd; + background-color: #fcfcfc; +} + +/* Edit section links */ +.editsection { + float: right; + margin-left: 5px; +} + +/** + * File histories + */ +table.filehistory { + border:1px solid #ccc; + border-collapse:collapse; +} + +table.filehistory th, +table.filehistory td { + padding: 0 0.2em 0 0.2em; + vertical-align:top; + border:1px solid #ccc; +} +table.filehistory th { + text-align: left; +} +table.filehistory td.mw-imagepage-filesize, +table.filehistory th.mw-imagepage-filesize { + white-space:nowrap; +} diff --git a/skins/common/upload.js b/skins/common/upload.js index 160fbf27..7cbfc9aa 100644 --- a/skins/common/upload.js +++ b/skins/common/upload.js @@ -1,23 +1,176 @@ function licenseSelectorCheck() { - var selector = document.getElementById("wpLicense"); - if (selector.selectedIndex > 0 && - selector.options[selector.selectedIndex].value == "" ) { - // Browser is broken, doesn't respect disabled attribute on