From ca32f08966f1b51fcb19460f0996bb0c4048e6fe Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 3 Dec 2011 13:29:22 +0100 Subject: Update to MediaWiki 1.18.0 * also update ArchLinux skin to chagnes in MonoBook * Use only css to hide our menu bar when printing --- includes/Action.php | 467 ++ includes/AjaxDispatcher.php | 15 +- includes/AjaxFunctions.php | 101 - includes/AjaxResponse.php | 4 - includes/Article.php | 4023 +++---------- includes/AuthPlugin.php | 13 + includes/AutoLoader.php | 522 +- includes/Autopromote.php | 72 +- includes/BacklinkCache.php | 131 +- includes/BagOStuff.php | 906 --- includes/Block.php | 1156 ++-- includes/CacheDependency.php | 367 -- includes/Category.php | 11 +- includes/CategoryPage.php | 197 +- includes/Categoryfinder.php | 7 +- includes/Cdb.php | 12 + includes/Cdb_PHP.php | 76 +- includes/ChangeTags.php | 36 +- includes/ChangesFeed.php | 54 +- includes/ChangesList.php | 564 +- includes/Collation.php | 52 +- includes/ConfEditor.php | 78 +- includes/Cookie.php | 245 + includes/Credits.php | 238 - includes/DefaultSettings.php | 833 ++- includes/Defines.php | 41 +- includes/DjVuImage.php | 12 +- includes/EditPage.php | 905 +-- includes/Exception.php | 351 +- includes/Exif.php | 1150 ---- includes/Export.php | 448 +- includes/ExternalEdit.php | 63 +- includes/ExternalStore.php | 16 +- includes/ExternalStoreDB.php | 16 +- includes/ExternalUser.php | 4 +- includes/FakeTitle.php | 6 +- includes/Fallback.php | 200 + includes/Feed.php | 52 +- includes/FeedUtils.php | 2 +- includes/FileDeleteForm.php | 36 +- includes/FileRevertForm.php | 180 - includes/ForkController.php | 6 +- includes/FormOptions.php | 121 +- includes/GenderCache.php | 135 + includes/GlobalFunctions.php | 2202 +++---- includes/HTMLCacheUpdate.php | 232 - includes/HTMLFileCache.php | 233 - includes/HTMLForm.php | 507 +- includes/HistoryBlob.php | 88 +- includes/HistoryPage.php | 50 +- includes/Hooks.php | 384 +- includes/Html.php | 102 +- includes/HttpFunctions.old.php | 1 + includes/HttpFunctions.php | 306 +- includes/IP.php | 86 + includes/ImageFunctions.php | 2 +- includes/ImageGallery.php | 147 +- includes/ImagePage.php | 660 ++- includes/ImageQueryPage.php | 29 +- includes/Import.php | 290 +- includes/Init.php | 184 + includes/Interwiki.php | 266 - includes/Licenses.php | 49 +- includes/LinkBatch.php | 166 - includes/LinkCache.php | 201 - includes/LinkFilter.php | 36 - includes/Linker.php | 971 ++- includes/LinksUpdate.php | 44 +- includes/LocalisationCache.php | 130 +- includes/LogEventsList.php | 277 +- includes/LogPage.php | 135 +- includes/MWFunction.php | 64 + includes/MacBinary.php | 272 - includes/MagicWord.php | 169 +- includes/Math.php | 341 -- includes/MemcachedSessions.php | 95 - includes/Message.php | 194 +- includes/MessageBlobStore.php | 56 +- includes/MessageCache.php | 792 --- includes/Metadata.php | 146 +- includes/MimeMagic.php | 475 +- includes/Namespace.php | 106 +- includes/ObjectCache.php | 123 - includes/OutputHandler.php | 22 +- includes/OutputPage.php | 1557 ++--- includes/PHPVersionError.php | 91 + includes/PageQueryPage.php | 2 +- includes/Pager.php | 40 +- includes/PatrolLog.php | 12 +- includes/PoolCounter.php | 115 +- includes/Preferences.php | 244 +- includes/PrefixSearch.php | 40 +- includes/Profiler.php | 463 -- includes/ProfilerSimple.php | 130 - includes/ProfilerSimpleText.php | 39 - includes/ProfilerSimpleTrace.php | 71 - includes/ProfilerSimpleUDP.php | 41 - includes/ProfilerStub.php | 52 - includes/ProtectionForm.php | 80 +- includes/ProxyTools.php | 107 +- includes/QueryPage.php | 393 +- includes/RawPage.php | 38 +- includes/RecentChange.php | 157 +- includes/RequestContext.php | 399 ++ includes/Revision.php | 124 +- includes/RevisionList.php | 370 ++ includes/Sanitizer.php | 760 +-- includes/SeleniumWebSettings.php | 197 +- includes/Setup.php | 463 +- includes/SiteConfiguration.php | 43 +- includes/SiteStats.php | 148 +- includes/Skin.php | 1783 ++---- includes/SkinLegacy.php | 942 +++ includes/SkinTemplate.php | 1275 +++- includes/SpecialPage.php | 1164 ++-- includes/SpecialPageFactory.php | 554 ++ includes/SquidPurgeClient.php | 28 + includes/SquidUpdate.php | 202 - includes/Status.php | 54 +- includes/StreamFile.php | 15 +- includes/StringUtils.php | 22 +- includes/StubObject.php | 22 +- includes/Title.php | 1394 ++--- includes/TitleArray.php | 22 +- includes/User.php | 1740 +++--- includes/UserArray.php | 35 +- includes/UserMailer.php | 183 +- includes/ViewCountUpdate.php | 111 + includes/WatchedItem.php | 5 + includes/WatchlistEditor.php | 528 -- includes/WebRequest.php | 375 +- includes/WebResponse.php | 130 +- includes/WebStart.php | 82 +- includes/Wiki.php | 639 +- includes/WikiCategoryPage.php | 37 + includes/WikiError.php | 12 + includes/WikiFilePage.php | 139 + includes/WikiMap.php | 21 +- includes/WikiPage.php | 2677 +++++++++ includes/Xml.php | 202 +- includes/XmlTypeCheck.php | 17 +- includes/ZhClient.php | 88 +- includes/ZhConversion.php | 230 +- includes/ZipDirectoryReader.php | 684 +++ includes/actions/CreditsAction.php | 235 + includes/actions/DeletetrackbackAction.php | 54 + includes/actions/InfoAction.php | 151 + includes/actions/MarkpatrolledAction.php | 86 + includes/actions/PurgeAction.php | 100 + includes/actions/RevertAction.php | 140 + includes/actions/RevisiondeleteAction.php | 53 + includes/actions/RollbackAction.php | 122 + includes/actions/WatchAction.php | 183 + includes/api/ApiBase.php | 276 +- includes/api/ApiBlock.php | 85 +- includes/api/ApiComparePages.php | 130 + includes/api/ApiDelete.php | 42 +- includes/api/ApiDisabled.php | 6 +- includes/api/ApiEditPage.php | 105 +- includes/api/ApiEmailUser.php | 8 +- includes/api/ApiExpandTemplates.php | 21 +- includes/api/ApiFeedContributions.php | 207 + includes/api/ApiFeedWatchlist.php | 41 +- includes/api/ApiFileRevert.php | 189 + includes/api/ApiFormatBase.php | 22 +- includes/api/ApiFormatDbg.php | 6 +- includes/api/ApiFormatDump.php | 4 +- includes/api/ApiFormatJson.php | 4 +- includes/api/ApiFormatPhp.php | 4 +- includes/api/ApiFormatRaw.php | 8 +- includes/api/ApiFormatTxt.php | 6 +- includes/api/ApiFormatWddx.php | 4 +- includes/api/ApiFormatXml.php | 24 +- includes/api/ApiFormatYaml.php | 14 +- includes/api/ApiHelp.php | 17 +- includes/api/ApiImport.php | 38 +- includes/api/ApiLogin.php | 25 +- includes/api/ApiLogout.php | 10 +- includes/api/ApiMain.php | 256 +- includes/api/ApiMove.php | 49 +- includes/api/ApiOpenSearch.php | 12 +- includes/api/ApiPageSet.php | 127 +- includes/api/ApiParamInfo.php | 83 +- includes/api/ApiParse.php | 209 +- includes/api/ApiPatrol.php | 10 +- includes/api/ApiProtect.php | 24 +- includes/api/ApiPurge.php | 47 +- includes/api/ApiQuery.php | 74 +- includes/api/ApiQueryAllCategories.php | 28 +- includes/api/ApiQueryAllLinks.php | 31 +- includes/api/ApiQueryAllUsers.php | 189 +- includes/api/ApiQueryAllimages.php | 68 +- includes/api/ApiQueryAllmessages.php | 135 +- includes/api/ApiQueryAllpages.php | 74 +- includes/api/ApiQueryBacklinks.php | 80 +- includes/api/ApiQueryBase.php | 104 +- includes/api/ApiQueryBlocks.php | 86 +- includes/api/ApiQueryCategories.php | 14 +- includes/api/ApiQueryCategoryInfo.php | 8 +- includes/api/ApiQueryCategoryMembers.php | 88 +- includes/api/ApiQueryDeletedrevs.php | 117 +- includes/api/ApiQueryDisabled.php | 6 +- includes/api/ApiQueryDuplicateFiles.php | 14 +- includes/api/ApiQueryExtLinksUsage.php | 109 +- includes/api/ApiQueryExternalLinks.php | 52 +- includes/api/ApiQueryFilearchive.php | 113 +- includes/api/ApiQueryIWBacklinks.php | 13 +- includes/api/ApiQueryIWLinks.php | 33 +- includes/api/ApiQueryImageInfo.php | 227 +- includes/api/ApiQueryImages.php | 28 +- includes/api/ApiQueryInfo.php | 50 +- includes/api/ApiQueryLangBacklinks.php | 220 + includes/api/ApiQueryLangLinks.php | 39 +- includes/api/ApiQueryLinks.php | 18 +- includes/api/ApiQueryLogEvents.php | 82 +- includes/api/ApiQueryPageProps.php | 62 +- includes/api/ApiQueryProtectedTitles.php | 32 +- includes/api/ApiQueryQueryPage.php | 198 + includes/api/ApiQueryRandom.php | 22 +- includes/api/ApiQueryRecentChanges.php | 132 +- includes/api/ApiQueryRevisions.php | 57 +- includes/api/ApiQuerySearch.php | 38 +- includes/api/ApiQuerySiteinfo.php | 140 +- includes/api/ApiQueryStashImageInfo.php | 93 +- includes/api/ApiQueryTags.php | 20 +- includes/api/ApiQueryUserContributions.php | 30 +- includes/api/ApiQueryUserInfo.php | 41 +- includes/api/ApiQueryUsers.php | 97 +- includes/api/ApiQueryWatchlist.php | 94 +- includes/api/ApiQueryWatchlistRaw.php | 14 +- includes/api/ApiResult.php | 35 +- includes/api/ApiRollback.php | 24 +- includes/api/ApiRsd.php | 15 +- includes/api/ApiUnblock.php | 46 +- includes/api/ApiUndelete.php | 19 +- includes/api/ApiUpload.php | 291 +- includes/api/ApiUserrights.php | 17 +- includes/api/ApiWatch.php | 33 +- includes/cache/CacheDependency.php | 394 ++ includes/cache/HTMLCacheUpdate.php | 230 + includes/cache/HTMLFileCache.php | 250 + includes/cache/LinkBatch.php | 195 + includes/cache/LinkCache.php | 219 + includes/cache/MemcachedSessions.php | 98 + includes/cache/MessageCache.php | 971 +++ includes/cache/SquidUpdate.php | 226 + includes/db/CloneDatabase.php | 158 + includes/db/Database.php | 1763 +++--- includes/db/DatabaseError.php | 314 + includes/db/DatabaseIbm_db2.php | 292 +- includes/db/DatabaseMssql.php | 484 +- includes/db/DatabaseMysql.php | 219 +- includes/db/DatabaseOracle.php | 290 +- includes/db/DatabasePostgres.php | 178 +- includes/db/DatabaseSqlite.php | 299 +- includes/db/DatabaseUtility.php | 268 + includes/db/LBFactory.php | 47 +- includes/db/LBFactory_Multi.php | 22 + includes/db/LBFactory_Single.php | 42 +- includes/db/LoadBalancer.php | 203 +- includes/db/LoadMonitor.php | 67 +- includes/diff/DairikiDiff.php | 1295 ++++ includes/diff/DifferenceEngine.php | 259 +- includes/diff/WikiDiff.php | 1241 ---- includes/diff/WikiDiff3.php | 14 +- includes/extauth/MediaWiki.php | 35 +- includes/extauth/vB.php | 6 +- includes/filerepo/ArchivedFile.php | 69 +- includes/filerepo/FSRepo.php | 122 +- includes/filerepo/File.php | 541 +- includes/filerepo/FileRepo.php | 122 +- includes/filerepo/FileRepoStatus.php | 12 + includes/filerepo/ForeignAPIFile.php | 29 +- includes/filerepo/ForeignAPIRepo.php | 51 +- includes/filerepo/ForeignDBFile.php | 23 +- includes/filerepo/ForeignDBRepo.php | 4 +- includes/filerepo/Image.php | 80 - includes/filerepo/LocalFile.php | 273 +- includes/filerepo/LocalRepo.php | 37 +- includes/filerepo/NullRepo.php | 3 + includes/filerepo/OldLocalFile.php | 100 +- includes/filerepo/README | 18 + includes/filerepo/RepoGroup.php | 56 +- includes/filerepo/UnregisteredLocalFile.php | 24 +- includes/installer/CliInstaller.php | 21 +- includes/installer/DatabaseInstaller.php | 27 +- includes/installer/DatabaseUpdater.php | 82 +- includes/installer/Ibm_db2Installer.php | 251 + includes/installer/Ibm_db2Updater.php | 69 + includes/installer/InstallDocFormatter.php | 42 + includes/installer/Installer.i18n.php | 6173 ++++++++++++++++---- includes/installer/Installer.php | 115 +- includes/installer/LocalSettingsGenerator.php | 19 +- includes/installer/MysqlInstaller.php | 62 +- includes/installer/MysqlUpdater.php | 36 +- includes/installer/OracleInstaller.php | 6 +- includes/installer/OracleUpdater.php | 35 +- includes/installer/PhpBugTests.php | 2 +- includes/installer/PostgresInstaller.php | 54 +- includes/installer/PostgresUpdater.php | 2 - includes/installer/SqliteInstaller.php | 43 +- includes/installer/SqliteUpdater.php | 7 + includes/installer/WebInstaller.php | 89 +- includes/installer/WebInstallerOutput.php | 27 +- includes/installer/WebInstallerPage.php | 114 +- includes/interwiki/Interwiki.php | 272 + includes/job/DoubleRedirectJob.php | 10 +- includes/job/JobQueue.php | 74 +- includes/job/RefreshLinksJob.php | 2 +- includes/job/UploadFromUrlJob.php | 61 +- includes/json/FormatJson.php | 7 +- includes/json/Services_JSON.php | 80 +- includes/libs/CSSMin.php | 52 +- includes/libs/HttpStatus.php | 68 + includes/libs/jsminplus.php | 2094 +++++++ includes/libs/spyc.php | 248 - includes/media/BMP.php | 35 +- includes/media/Bitmap.php | 363 +- includes/media/BitmapMetadataHandler.php | 269 + includes/media/Bitmap_ClientOnly.php | 14 + includes/media/DjVu.php | 65 +- includes/media/Exif.php | 836 +++ includes/media/ExifBitmap.php | 210 + includes/media/FormatMetadata.php | 1354 +++++ includes/media/GIF.php | 103 +- includes/media/GIFMetadataExtractor.php | 236 +- includes/media/Generic.php | 302 +- includes/media/IPTC.php | 576 ++ includes/media/Jpeg.php | 46 + includes/media/JpegMetadataExtractor.php | 252 + includes/media/MediaTransformOutput.php | 26 +- includes/media/PNG.php | 88 +- includes/media/PNGMetadataExtractor.php | 359 +- includes/media/SVG.php | 92 +- includes/media/SVGMetadataExtractor.php | 27 +- includes/media/Tiff.php | 51 +- includes/media/XMP.php | 1174 ++++ includes/media/XMPInfo.php | 1139 ++++ includes/media/XMPValidate.php | 323 + includes/memcached-client.php | 1096 ---- includes/mime.info | 3 +- includes/mime.types | 2 + includes/normal/CleanUpTest.php | 425 -- includes/normal/Makefile | 10 +- includes/normal/Utf8Case.php | 30 + includes/normal/Utf8CaseGenerate.php | 1 + includes/normal/Utf8Test.php | 2 + includes/normal/UtfNormal.php | 42 +- includes/normal/UtfNormalBench.php | 1 + includes/normal/UtfNormalData.inc | 10 +- includes/normal/UtfNormalDataK.inc | 2 +- includes/normal/UtfNormalDefines.php | 6 +- includes/normal/UtfNormalGenerate.php | 1 + includes/normal/UtfNormalMemStress.php | 110 + includes/normal/UtfNormalTest.php | 1 + includes/normal/UtfNormalTest2.php | 6 +- includes/normal/UtfNormalUtil.php | 6 +- includes/objectcache/APCBagOStuff.php | 43 + includes/objectcache/BagOStuff.php | 164 + includes/objectcache/DBABagOStuff.php | 194 + includes/objectcache/EhcacheBagOStuff.php | 230 + includes/objectcache/EmptyBagOStuff.php | 27 + includes/objectcache/HashBagOStuff.php | 58 + includes/objectcache/MemcachedClient.php | 1115 ++++ includes/objectcache/MemcachedPhpBagOStuff.php | 178 + includes/objectcache/MultiWriteBagOStuff.php | 113 + includes/objectcache/ObjectCache.php | 119 + includes/objectcache/SqlBagOStuff.php | 432 ++ includes/objectcache/WinCacheBagOStuff.php | 71 + includes/objectcache/XCacheBagOStuff.php | 51 + includes/objectcache/eAccelBagOStuff.php | 46 + includes/parser/CoreLinkFunctions.php | 30 +- includes/parser/CoreParserFunctions.php | 173 +- includes/parser/CoreTagHooks.php | 65 +- includes/parser/DateFormatter.php | 8 +- includes/parser/LinkHolderArray.php | 252 +- includes/parser/Parser.php | 1224 ++-- includes/parser/ParserCache.php | 44 +- includes/parser/ParserOptions.php | 192 +- includes/parser/ParserOutput.php | 133 +- includes/parser/Parser_DiffTest.php | 4 + includes/parser/Parser_LinkHooks.php | 21 +- includes/parser/Preprocessor.php | 48 +- includes/parser/Preprocessor_DOM.php | 249 +- includes/parser/Preprocessor_Hash.php | 214 +- includes/parser/Preprocessor_HipHop.hphp | 1941 ++++++ includes/parser/StripState.php | 175 + includes/parser/Tidy.php | 163 +- includes/profiler/Profiler.php | 484 ++ includes/profiler/ProfilerSimple.php | 127 + includes/profiler/ProfilerSimpleText.php | 54 + includes/profiler/ProfilerSimpleTrace.php | 67 + includes/profiler/ProfilerSimpleUDP.php | 41 + includes/profiler/ProfilerStub.php | 15 + includes/proxy_check.php | 54 - includes/resourceloader/ResourceLoader.php | 297 +- includes/resourceloader/ResourceLoaderContext.php | 50 +- .../resourceloader/ResourceLoaderFileModule.php | 270 +- .../ResourceLoaderFilePageModule.php | 11 + includes/resourceloader/ResourceLoaderModule.php | 189 +- .../ResourceLoaderNoscriptModule.php | 52 + .../resourceloader/ResourceLoaderSiteModule.php | 4 +- .../resourceloader/ResourceLoaderStartUpModule.php | 143 +- .../ResourceLoaderUserGroupsModule.php | 59 + .../resourceloader/ResourceLoaderUserModule.php | 12 +- .../ResourceLoaderUserOptionsModule.php | 29 +- .../ResourceLoaderUserTokensModule.php | 63 + .../resourceloader/ResourceLoaderWikiModule.php | 44 +- includes/revisiondelete/RevisionDelete.php | 374 +- .../revisiondelete/RevisionDeleteAbstracts.php | 235 +- includes/revisiondelete/RevisionDeleteUser.php | 130 + includes/revisiondelete/RevisionDeleter.php | 191 +- includes/search/SearchEngine.php | 124 +- includes/search/SearchIBM_DB2.php | 4 +- includes/search/SearchMssql.php | 8 +- includes/search/SearchMySQL.php | 190 +- includes/search/SearchOracle.php | 14 +- includes/search/SearchPostgres.php | 36 +- includes/search/SearchSqlite.php | 12 +- includes/search/SearchUpdate.php | 4 +- includes/specials/SpecialActiveusers.php | 14 + includes/specials/SpecialAllmessages.php | 214 +- includes/specials/SpecialAllpages.php | 80 +- includes/specials/SpecialAncientpages.php | 40 +- includes/specials/SpecialBlankpage.php | 3 +- includes/specials/SpecialBlock.php | 855 +++ includes/specials/SpecialBlockList.php | 437 ++ includes/specials/SpecialBlockip.php | 892 --- includes/specials/SpecialBlockme.php | 8 +- includes/specials/SpecialBooksources.php | 41 +- includes/specials/SpecialBrokenRedirects.php | 75 +- includes/specials/SpecialCategories.php | 19 +- includes/specials/SpecialChangePassword.php | 248 + includes/specials/SpecialComparePages.php | 170 +- includes/specials/SpecialConfirmemail.php | 79 +- includes/specials/SpecialContributions.php | 322 +- includes/specials/SpecialDeadendpages.php | 58 +- includes/specials/SpecialDeletedContributions.php | 31 +- includes/specials/SpecialDisambiguations.php | 109 +- includes/specials/SpecialDoubleRedirects.php | 86 +- includes/specials/SpecialEditWatchlist.php | 596 ++ includes/specials/SpecialEmailuser.php | 112 +- includes/specials/SpecialExport.php | 127 +- includes/specials/SpecialFewestrevisions.php | 41 +- includes/specials/SpecialFileDuplicateSearch.php | 226 +- includes/specials/SpecialFilepath.php | 11 +- includes/specials/SpecialImport.php | 50 +- includes/specials/SpecialIpblocklist.php | 581 -- includes/specials/SpecialLinkSearch.php | 202 +- includes/specials/SpecialListfiles.php | 148 +- includes/specials/SpecialListgrouprights.php | 62 +- includes/specials/SpecialListredirects.php | 96 +- includes/specials/SpecialListusers.php | 86 +- includes/specials/SpecialLockdb.php | 24 +- includes/specials/SpecialLog.php | 9 +- includes/specials/SpecialLonelypages.php | 64 +- includes/specials/SpecialLongpages.php | 15 +- includes/specials/SpecialMIMEsearch.php | 151 +- includes/specials/SpecialMergeHistory.php | 210 +- includes/specials/SpecialMostcategories.php | 49 +- includes/specials/SpecialMostimages.php | 39 +- includes/specials/SpecialMostlinked.php | 62 +- includes/specials/SpecialMostlinkedcategories.php | 57 +- includes/specials/SpecialMostlinkedtemplates.php | 50 +- includes/specials/SpecialMostrevisions.php | 62 +- includes/specials/SpecialMovepage.php | 36 +- includes/specials/SpecialNewimages.php | 308 +- includes/specials/SpecialNewpages.php | 286 +- includes/specials/SpecialPasswordReset.php | 273 + includes/specials/SpecialPopularpages.php | 51 +- includes/specials/SpecialPreferences.php | 5 +- includes/specials/SpecialPrefixindex.php | 58 +- includes/specials/SpecialProtectedpages.php | 39 +- includes/specials/SpecialProtectedtitles.php | 26 +- includes/specials/SpecialRandompage.php | 90 +- includes/specials/SpecialRecentchanges.php | 417 +- includes/specials/SpecialRecentchangeslinked.php | 52 +- includes/specials/SpecialResetpass.php | 228 - includes/specials/SpecialRevisiondelete.php | 238 +- includes/specials/SpecialSearch.php | 412 +- includes/specials/SpecialShortpages.php | 60 +- includes/specials/SpecialSpecialpages.php | 44 +- includes/specials/SpecialStatistics.php | 121 +- includes/specials/SpecialTags.php | 16 +- includes/specials/SpecialUnblock.php | 209 + .../specials/SpecialUncategorizedcategories.php | 18 +- includes/specials/SpecialUncategorizedimages.php | 32 +- includes/specials/SpecialUncategorizedpages.php | 54 +- .../specials/SpecialUncategorizedtemplates.php | 18 +- includes/specials/SpecialUndelete.php | 703 +-- includes/specials/SpecialUnlockdb.php | 16 +- includes/specials/SpecialUnusedcategories.php | 34 +- includes/specials/SpecialUnusedimages.php | 70 +- includes/specials/SpecialUnusedtemplates.php | 39 +- includes/specials/SpecialUnwatchedpages.php | 66 +- includes/specials/SpecialUpload.php | 150 +- includes/specials/SpecialUploadStash.php | 168 +- includes/specials/SpecialUserlogin.php | 405 +- includes/specials/SpecialUserrights.php | 29 +- includes/specials/SpecialVersion.php | 303 +- includes/specials/SpecialWantedcategories.php | 48 +- includes/specials/SpecialWantedfiles.php | 45 +- includes/specials/SpecialWantedpages.php | 102 +- includes/specials/SpecialWantedtemplates.php | 44 +- includes/specials/SpecialWatchlist.php | 825 +-- includes/specials/SpecialWhatlinkshere.php | 86 +- includes/specials/SpecialWithoutinterwiki.php | 66 +- includes/templates/PHP4.php | 102 - includes/templates/Userlogin.php | 48 +- includes/upload/UploadBase.php | 314 +- includes/upload/UploadFromFile.php | 33 +- includes/upload/UploadFromStash.php | 130 +- includes/upload/UploadFromUrl.php | 31 +- includes/upload/UploadStash.php | 615 +- includes/zhtable/Makefile.py | 14 +- includes/zhtable/simp2trad_noconvert.manual | 137 +- includes/zhtable/simpphrases.manual | 6 +- includes/zhtable/toCN.manual | 9 +- includes/zhtable/toHK.manual | 4 +- includes/zhtable/toSimp.manual | 5 +- includes/zhtable/toTW.manual | 8 +- includes/zhtable/toTrad.manual | 6 +- includes/zhtable/trad2simp.manual | 144 +- includes/zhtable/trad2simp_noconvert.manual | 1 + includes/zhtable/tradphrases.manual | 50 +- includes/zhtable/tradphrases_exclude.manual | 1 + 526 files changed, 71183 insertions(+), 39453 deletions(-) create mode 100644 includes/Action.php delete mode 100644 includes/AjaxFunctions.php delete mode 100644 includes/BagOStuff.php delete mode 100644 includes/CacheDependency.php create mode 100644 includes/Cookie.php delete mode 100644 includes/Credits.php delete mode 100644 includes/Exif.php create mode 100644 includes/Fallback.php delete mode 100644 includes/FileRevertForm.php create mode 100644 includes/GenderCache.php delete mode 100644 includes/HTMLCacheUpdate.php delete mode 100644 includes/HTMLFileCache.php create mode 100644 includes/Init.php delete mode 100644 includes/Interwiki.php delete mode 100644 includes/LinkBatch.php delete mode 100644 includes/LinkCache.php create mode 100644 includes/MWFunction.php delete mode 100644 includes/MacBinary.php delete mode 100644 includes/Math.php delete mode 100644 includes/MemcachedSessions.php delete mode 100644 includes/MessageCache.php delete mode 100644 includes/ObjectCache.php create mode 100644 includes/PHPVersionError.php delete mode 100644 includes/Profiler.php delete mode 100644 includes/ProfilerSimple.php delete mode 100644 includes/ProfilerSimpleText.php delete mode 100644 includes/ProfilerSimpleTrace.php delete mode 100644 includes/ProfilerSimpleUDP.php delete mode 100644 includes/ProfilerStub.php create mode 100644 includes/RequestContext.php create mode 100644 includes/RevisionList.php create mode 100644 includes/SkinLegacy.php create mode 100644 includes/SpecialPageFactory.php delete mode 100644 includes/SquidUpdate.php create mode 100644 includes/ViewCountUpdate.php delete mode 100644 includes/WatchlistEditor.php create mode 100644 includes/WikiCategoryPage.php create mode 100644 includes/WikiFilePage.php create mode 100644 includes/WikiPage.php create mode 100644 includes/ZipDirectoryReader.php create mode 100644 includes/actions/CreditsAction.php create mode 100644 includes/actions/DeletetrackbackAction.php create mode 100644 includes/actions/InfoAction.php create mode 100644 includes/actions/MarkpatrolledAction.php create mode 100644 includes/actions/PurgeAction.php create mode 100644 includes/actions/RevertAction.php create mode 100644 includes/actions/RevisiondeleteAction.php create mode 100644 includes/actions/RollbackAction.php create mode 100644 includes/actions/WatchAction.php create mode 100644 includes/api/ApiComparePages.php create mode 100644 includes/api/ApiFeedContributions.php create mode 100644 includes/api/ApiFileRevert.php create mode 100644 includes/api/ApiQueryLangBacklinks.php create mode 100644 includes/api/ApiQueryQueryPage.php create mode 100644 includes/cache/CacheDependency.php create mode 100644 includes/cache/HTMLCacheUpdate.php create mode 100644 includes/cache/HTMLFileCache.php create mode 100644 includes/cache/LinkBatch.php create mode 100644 includes/cache/LinkCache.php create mode 100644 includes/cache/MemcachedSessions.php create mode 100644 includes/cache/MessageCache.php create mode 100644 includes/cache/SquidUpdate.php create mode 100644 includes/db/CloneDatabase.php create mode 100644 includes/db/DatabaseError.php create mode 100644 includes/db/DatabaseUtility.php create mode 100644 includes/diff/DairikiDiff.php delete mode 100644 includes/diff/WikiDiff.php delete mode 100644 includes/filerepo/Image.php create mode 100644 includes/installer/Ibm_db2Installer.php create mode 100644 includes/installer/Ibm_db2Updater.php create mode 100644 includes/installer/InstallDocFormatter.php create mode 100644 includes/interwiki/Interwiki.php create mode 100644 includes/libs/HttpStatus.php create mode 100644 includes/libs/jsminplus.php delete mode 100644 includes/libs/spyc.php create mode 100644 includes/media/BitmapMetadataHandler.php create mode 100644 includes/media/Exif.php create mode 100644 includes/media/ExifBitmap.php create mode 100644 includes/media/FormatMetadata.php create mode 100644 includes/media/IPTC.php create mode 100644 includes/media/Jpeg.php create mode 100644 includes/media/JpegMetadataExtractor.php create mode 100644 includes/media/XMP.php create mode 100644 includes/media/XMPInfo.php create mode 100644 includes/media/XMPValidate.php delete mode 100644 includes/memcached-client.php delete mode 100644 includes/normal/CleanUpTest.php create mode 100644 includes/normal/UtfNormalMemStress.php create mode 100644 includes/objectcache/APCBagOStuff.php create mode 100644 includes/objectcache/BagOStuff.php create mode 100644 includes/objectcache/DBABagOStuff.php create mode 100644 includes/objectcache/EhcacheBagOStuff.php create mode 100644 includes/objectcache/EmptyBagOStuff.php create mode 100644 includes/objectcache/HashBagOStuff.php create mode 100644 includes/objectcache/MemcachedClient.php create mode 100644 includes/objectcache/MemcachedPhpBagOStuff.php create mode 100644 includes/objectcache/MultiWriteBagOStuff.php create mode 100644 includes/objectcache/ObjectCache.php create mode 100644 includes/objectcache/SqlBagOStuff.php create mode 100644 includes/objectcache/WinCacheBagOStuff.php create mode 100644 includes/objectcache/XCacheBagOStuff.php create mode 100644 includes/objectcache/eAccelBagOStuff.php create mode 100644 includes/parser/Preprocessor_HipHop.hphp create mode 100644 includes/parser/StripState.php create mode 100644 includes/profiler/Profiler.php create mode 100644 includes/profiler/ProfilerSimple.php create mode 100644 includes/profiler/ProfilerSimpleText.php create mode 100644 includes/profiler/ProfilerSimpleTrace.php create mode 100644 includes/profiler/ProfilerSimpleUDP.php create mode 100644 includes/profiler/ProfilerStub.php delete mode 100644 includes/proxy_check.php create mode 100644 includes/resourceloader/ResourceLoaderFilePageModule.php create mode 100644 includes/resourceloader/ResourceLoaderNoscriptModule.php create mode 100644 includes/resourceloader/ResourceLoaderUserGroupsModule.php create mode 100644 includes/resourceloader/ResourceLoaderUserTokensModule.php create mode 100644 includes/revisiondelete/RevisionDeleteUser.php create mode 100644 includes/specials/SpecialBlock.php create mode 100644 includes/specials/SpecialBlockList.php delete mode 100644 includes/specials/SpecialBlockip.php create mode 100644 includes/specials/SpecialChangePassword.php create mode 100644 includes/specials/SpecialEditWatchlist.php delete mode 100644 includes/specials/SpecialIpblocklist.php create mode 100644 includes/specials/SpecialPasswordReset.php delete mode 100644 includes/specials/SpecialResetpass.php create mode 100644 includes/specials/SpecialUnblock.php delete mode 100644 includes/templates/PHP4.php (limited to 'includes') diff --git a/includes/Action.php b/includes/Action.php new file mode 100644 index 00000000..d5432b23 --- /dev/null +++ b/includes/Action.php @@ -0,0 +1,467 @@ +getActionOverrides() ); + if ( $class ) { + $obj = new $class( $page ); + return $obj; + } + return $class; + } + + /** + * Check if a given action is recognised, even if it's disabled + * + * @param $name String: name of an action + * @return Bool + */ + public final static function exists( $name ) { + return self::getClass( $name ) !== null; + } + + /** + * Get the IContextSource in use here + * @return IContextSource + */ + protected final function getContext() { + if ( $this->context instanceof IContextSource ) { + return $this->context; + } + return $this->page->getContext(); + } + + /** + * Get the WebRequest being used for this instance + * + * @return WebRequest + */ + protected final function getRequest() { + return $this->getContext()->getRequest(); + } + + /** + * Get the OutputPage being used for this instance + * + * @return OutputPage + */ + protected final function getOutput() { + return $this->getContext()->getOutput(); + } + + /** + * Shortcut to get the User being used for this instance + * + * @return User + */ + protected final function getUser() { + return $this->getContext()->getUser(); + } + + /** + * Shortcut to get the Skin being used for this instance + * + * @return Skin + */ + protected final function getSkin() { + return $this->getContext()->getSkin(); + } + + /** + * Shortcut to get the user Language being used for this instance + * + * @return Skin + */ + protected final function getLang() { + return $this->getContext()->getLang(); + } + + /** + * Shortcut to get the Title object from the page + * @return Title + */ + protected final function getTitle() { + return $this->page->getTitle(); + } + + /** + * Protected constructor: use Action::factory( $action, $page ) to actually build + * these things in the real world + * @param Page $page + */ + protected function __construct( Page $page ) { + $this->page = $page; + } + + /** + * Return the name of the action this object responds to + * @return String lowercase + */ + public abstract function getName(); + + /** + * Get the permission required to perform this action. Often, but not always, + * the same as the action name + */ + public abstract function getRestriction(); + + /** + * Checks if the given user (identified by an object) can perform this action. Can be + * overridden by sub-classes with more complicated permissions schemes. Failures here + * must throw subclasses of ErrorPageError + * + * @param $user User: the user to check, or null to use the context user + * @throws ErrorPageError + */ + protected function checkCanExecute( User $user ) { + if ( $this->requiresWrite() && wfReadOnly() ) { + throw new ReadOnlyError(); + } + + if ( $this->getRestriction() !== null && !$user->isAllowed( $this->getRestriction() ) ) { + throw new PermissionsError( $this->getRestriction() ); + } + + if ( $this->requiresUnblock() && $user->isBlocked() ) { + $block = $user->mBlock; + throw new UserBlockedError( $block ); + } + } + + /** + * Whether this action requires the wiki not to be locked + * @return Bool + */ + public function requiresWrite() { + return true; + } + + /** + * Whether this action can still be executed by a blocked user + * @return Bool + */ + public function requiresUnblock() { + return true; + } + + /** + * Set output headers for noindexing etc. This function will not be called through + * the execute() entry point, so only put UI-related stuff in here. + */ + protected function setHeaders() { + $out = $this->getOutput(); + $out->setRobotPolicy( "noindex,nofollow" ); + $out->setPageTitle( $this->getPageTitle() ); + $this->getOutput()->setSubtitle( $this->getDescription() ); + $out->setArticleRelated( true ); + } + + /** + * Returns the name that goes in the \ page title + * + * @return String + */ + protected function getPageTitle() { + return $this->getTitle()->getPrefixedText(); + } + + /** + * Returns the description that goes below the \ tag + * + * @return String + */ + protected function getDescription() { + return wfMsg( strtolower( $this->getName() ) ); + } + + /** + * The main action entry point. Do all output for display and send it to the context + * output. Do not use globals $wgOut, $wgRequest, etc, in implementations; use + * $this->getOutput(), etc. + * @throws ErrorPageError + */ + public abstract function show(); + + /** + * Execute the action in a silent fashion: do not display anything or release any errors. + * @param $data Array values that would normally be in the POST request + * @param $captureErrors Bool whether to catch exceptions and just return false + * @return Bool whether execution was successful + */ + public abstract function execute(); +} + +abstract class FormAction extends Action { + + /** + * Get an HTMLForm descriptor array + * @return Array + */ + protected abstract function getFormFields(); + + /** + * Add pre- or post-text to the form + * @return String HTML which will be sent to $form->addPreText() + */ + protected function preText() { return ''; } + protected function postText() { return ''; } + + /** + * Play with the HTMLForm if you need to more substantially + * @param $form HTMLForm + */ + protected function alterForm( HTMLForm $form ) {} + + /** + * Get the HTMLForm to control behaviour + * @return HTMLForm|null + */ + protected function getForm() { + $this->fields = $this->getFormFields(); + + // Give hooks a chance to alter the form, adding extra fields or text etc + wfRunHooks( 'ActionModifyFormFields', array( $this->getName(), &$this->fields, $this->page ) ); + + $form = new HTMLForm( $this->fields, $this->getContext() ); + $form->setSubmitCallback( array( $this, 'onSubmit' ) ); + + // Retain query parameters (uselang etc) + $form->addHiddenField( 'action', $this->getName() ); // Might not be the same as the query string + $params = array_diff_key( + $this->getRequest()->getQueryValues(), + array( 'action' => null, 'title' => null ) + ); + $form->addHiddenField( 'redirectparams', wfArrayToCGI( $params ) ); + + $form->addPreText( $this->preText() ); + $form->addPostText( $this->postText() ); + $this->alterForm( $form ); + + // Give hooks a chance to alter the form, adding extra fields or text etc + wfRunHooks( 'ActionBeforeFormDisplay', array( $this->getName(), &$form, $this->page ) ); + + return $form; + } + + /** + * Process the form on POST submission. If you return false from getFormFields(), + * this will obviously never be reached. If you don't want to do anything with the + * form, just return false here + * @param $data Array + * @return Bool|Array true for success, false for didn't-try, array of errors on failure + */ + public abstract function onSubmit( $data ); + + /** + * Do something exciting on successful processing of the form. This might be to show + * a confirmation message (watch, rollback, etc) or to redirect somewhere else (edit, + * protect, etc). + */ + public abstract function onSuccess(); + + /** + * The basic pattern for actions is to display some sort of HTMLForm UI, maybe with + * some stuff underneath (history etc); to do some processing on submission of that + * form (delete, protect, etc) and to do something exciting on 'success', be that + * display something new or redirect to somewhere. Some actions have more exotic + * behaviour, but that's what subclassing is for :D + */ + public function show() { + $this->setHeaders(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $form = $this->getForm(); + if ( $form->show() ) { + $this->onSuccess(); + } + } + + /** + * @see Action::execute() + * @throws ErrorPageError + * @param array|null $data + * @param bool $captureErrors + * @return bool + */ + public function execute( array $data = null, $captureErrors = true ) { + try { + // Set a new context so output doesn't leak. + $this->context = clone $this->page->getContext(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $fields = array(); + foreach ( $this->fields as $key => $params ) { + if ( isset( $data[$key] ) ) { + $fields[$key] = $data[$key]; + } elseif ( isset( $params['default'] ) ) { + $fields[$key] = $params['default']; + } else { + $fields[$key] = null; + } + } + $status = $this->onSubmit( $fields ); + if ( $status === true ) { + // This might do permanent stuff + $this->onSuccess(); + return true; + } else { + return false; + } + } + catch ( ErrorPageError $e ) { + if ( $captureErrors ) { + return false; + } else { + throw $e; + } + } + } +} + +/** + * Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input + * format (protect, delete, move, etc), and the just-do-something format (watch, rollback, + * patrol, etc). + */ +abstract class FormlessAction extends Action { + + /** + * Show something on GET request. + * @return String|null will be added to the HTMLForm if present, or just added to the + * output if not. Return null to not add anything + */ + public abstract function onView(); + + /** + * We don't want an HTMLForm + */ + protected function getFormFields() { + return false; + } + + public function onSubmit( $data ) { + return false; + } + + public function onSuccess() { + return false; + } + + public function show() { + $this->setHeaders(); + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $this->getOutput()->addHTML( $this->onView() ); + } + + /** + * Execute the action silently, not giving any output. Since these actions don't have + * forms, they probably won't have any data, but some (eg rollback) may do + * @param $data Array values that would normally be in the GET request + * @param $captureErrors Bool whether to catch exceptions and just return false + * @return Bool whether execution was successful + */ + public function execute( array $data = null, $captureErrors = true ) { + try { + // Set a new context so output doesn't leak. + $this->context = clone $this->page->getContext(); + if ( is_array( $data ) ) { + $this->context->setRequest( new FauxRequest( $data, false ) ); + } + + // This will throw exceptions if there's a problem + $this->checkCanExecute( $this->getUser() ); + + $this->onView(); + return true; + } + catch ( ErrorPageError $e ) { + if ( $captureErrors ) { + return false; + } else { + throw $e; + } + } + } +} diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index f7583188..17b154d6 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -7,12 +7,6 @@ * Handle ajax requests and send them to the proper handler. */ -if ( !( defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) { - die( 1 ); -} - -require_once( 'AjaxFunctions.php' ); - /** * Object-Oriented Ajax functions. * @ingroup Ajax @@ -74,7 +68,7 @@ class AjaxDispatcher { * request. */ function performAction() { - global $wgAjaxExportList, $wgOut, $wgUser; + global $wgAjaxExportList, $wgOut; if ( empty( $this->mode ) ) { return; @@ -90,13 +84,6 @@ class AjaxDispatcher { 'Bad Request', "unknown function " . (string) $this->func_name ); - } elseif ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) - && !$wgUser->isAllowed( 'read' ) ) - { - wfHttpError( - 403, - 'Forbidden', - 'You must log in to view pages.' ); } else { wfDebug( __METHOD__ . ' dispatching ' . $this->func_name . "\n" ); diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php deleted file mode 100644 index 8e5de31b..00000000 --- a/includes/AjaxFunctions.php +++ /dev/null @@ -1,101 +0,0 @@ -> 6 ) + 192 ) . chr( ( $num&63 ) + 128 ); - } - - if ( $num < 65536 ) { - return chr( ( $num >> 12 ) + 224 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 ); - } - - if ( $num < 2097152 ) { - return chr( ( $num >> 18 ) + 240 ) . chr( ( ( $num >> 12 )&63 ) + 128 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 ); - } - - return ''; -} - -/** - * Called in some places (currently just extensions) - * to get the URL for a given file. - */ -function wfAjaxGetFileUrl( $file ) { - $file = wfFindFile( $file ); - - if ( !$file || !$file->exists() ) { - return null; - } - - $url = $file->getUrl(); - - return $url; -} diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 014798f8..b9f80855 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -6,10 +6,6 @@ * @ingroup Ajax */ -if ( !defined( 'MEDIAWIKI' ) ) { - die( 1 ); -} - /** * Handle responses for Ajax requests (send headers, print * content, that sort of thing) diff --git a/includes/Article.php b/includes/Article.php index 3e8cfd5e..a0cc6a95 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -5,7 +5,11 @@ */ /** - * Class representing a MediaWiki article and history. + * Class for viewing MediaWiki article and history. + * + * This maintains WikiPage functions for backwards compatibility. + * + * @TODO: move and rewrite code to an Action class * * See design.txt for an overview. * Note: edit user interface and cache support functions have been @@ -13,198 +17,125 @@ * * @internal documentation reviewed 15 Mar 2010 */ -class Article { +class Article extends Page { /**@{{ * @private */ - var $mComment = ''; // !< + + /** + * @var IContextSource + */ + protected $mContext; + + /** + * @var WikiPage + */ + protected $mPage; + var $mContent; // !< var $mContentLoaded = false; // !< - var $mCounter = -1; // !< Not loaded - var $mCurID = -1; // !< Not loaded - var $mDataLoaded = false; // !< - var $mForUpdate = false; // !< - var $mGoodAdjustment = 0; // !< - var $mIsRedirect = false; // !< - var $mLatest = false; // !< - var $mMinorEdit; // !< var $mOldId; // !< - var $mPreparedEdit = false; // !< Title object if set - var $mRedirectedFrom = null; // !< Title object if set - var $mRedirectTarget = null; // !< Title object if set + + /** + * @var Title + */ + var $mRedirectedFrom = null; + + /** + * @var mixed: boolean false or URL string + */ var $mRedirectUrl = false; // !< var $mRevIdFetched = 0; // !< - var $mRevision; // !< Revision object if set - var $mTimestamp = ''; // !< - var $mTitle; // !< Title object - var $mTotalAdjustment = 0; // !< - var $mTouched = '19700101000000'; // !< - var $mUser = -1; // !< Not loaded - var $mUserText = ''; // !< username from Revision if set - var $mParserOptions; // !< ParserOptions object - var $mParserOutput; // !< ParserCache object if set + + /** + * @var Revision + */ + var $mRevision = null; + + /** + * @var ParserOutput + */ + var $mParserOutput; + /**@}}*/ /** * Constructor and clear the article - * @param $title Reference to a Title object. + * @param $title Title Reference to a Title object. * @param $oldId Integer revision ID, null to fetch from request, zero for current */ public function __construct( Title $title, $oldId = null ) { - // FIXME: does the reference play any role here? - $this->mTitle =& $title; $this->mOldId = $oldId; + $this->mPage = $this->newPage( $title ); + } + + protected function newPage( Title $title ) { + return new WikiPage( $title ); } /** - * Constructor from an page id - * @param $id The article ID to load + * Constructor from a page id + * @param $id Int article ID to load */ public static function newFromID( $id ) { $t = Title::newFromID( $id ); - # FIXME: doesn't inherit right + # @todo FIXME: Doesn't inherit right return $t == null ? null : new self( $t ); # return $t == null ? null : new static( $t ); // PHP 5.3 } /** - * Tell the page view functions that this view was redirected - * from another page on the wiki. - * @param $from Title object. - */ - public function setRedirectedFrom( Title $from ) { - $this->mRedirectedFrom = $from; - } - - /** - * If this page is a redirect, get its target + * Create an Article object of the appropriate class for the given page. * - * The target will be fetched from the redirect table if possible. - * If this page doesn't have an entry there, call insertRedirect() - * @return mixed Title object, or null if this page is not a redirect + * @param $title Title + * @param $context IContextSource + * @return Article object */ - public function getRedirectTarget() { - if ( !$this->mTitle->isRedirect() ) { - return null; - } - - if ( $this->mRedirectTarget !== null ) { - return $this->mRedirectTarget; + public static function newFromTitle( $title, IContextSource $context ) { + if ( NS_MEDIA == $title->getNamespace() ) { + // FIXME: where should this go? + $title = Title::makeTitle( NS_FILE, $title->getDBkey() ); } - # Query the redirect table - $dbr = wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow( 'redirect', - array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ), - array( 'rd_from' => $this->getID() ), - __METHOD__ - ); - - // rd_fragment and rd_interwiki were added later, populate them if empty - if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { - return $this->mRedirectTarget = Title::makeTitle( - $row->rd_namespace, $row->rd_title, - $row->rd_fragment, $row->rd_interwiki ); + $page = null; + wfRunHooks( 'ArticleFromTitle', array( &$title, &$page ) ); + if ( !$page ) { + switch( $title->getNamespace() ) { + case NS_FILE: + $page = new ImagePage( $title ); + break; + case NS_CATEGORY: + $page = new CategoryPage( $title ); + break; + default: + $page = new Article( $title ); + } } + $page->setContext( $context ); - # This page doesn't have an entry in the redirect table - return $this->mRedirectTarget = $this->insertRedirect(); - } - - /** - * Insert an entry for this page into the redirect table. - * - * Don't call this function directly unless you know what you're doing. - * @return Title object or null if not a redirect - */ - public function insertRedirect() { - // recurse through to only get the final target - $retval = Title::newFromRedirectRecurse( $this->getContent() ); - if ( !$retval ) { - return null; - } - $this->insertRedirectEntry( $retval ); - return $retval; - } - - /** - * Insert or update the redirect table entry for this page to indicate - * it redirects to $rt . - * @param $rt Title redirect target - */ - public function insertRedirectEntry( $rt ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'redirect', array( 'rd_from' ), - array( - 'rd_from' => $this->getID(), - 'rd_namespace' => $rt->getNamespace(), - 'rd_title' => $rt->getDBkey(), - 'rd_fragment' => $rt->getFragment(), - 'rd_interwiki' => $rt->getInterwiki(), - ), - __METHOD__ - ); + return $page; } /** - * Get the Title object or URL this page redirects to + * Create an Article object of the appropriate class for the given page. * - * @return mixed false, Title of in-wiki target, or string with URL + * @param $page WikiPage + * @param $context IContextSource + * @return Article object */ - public function followRedirect() { - return $this->getRedirectURL( $this->getRedirectTarget() ); + public static function newFromWikiPage( WikiPage $page, IContextSource $context ) { + $article = self::newFromTitle( $page->getTitle(), $context ); + $article->mPage = $page; // override to keep process cached vars + return $article; } /** - * Get the Title object this text redirects to - * - * @param $text string article content containing redirect info - * @return mixed false, Title of in-wiki target, or string with URL - * @deprecated - */ - public function followRedirectText( $text ) { - // recurse through to only get the final target - return $this->getRedirectURL( Title::newFromRedirectRecurse( $text ) ); - } - - /** - * Get the Title object or URL to use for a redirect. We use Title - * objects for same-wiki, non-special redirects and URLs for everything - * else. - * @param $rt Title Redirect target - * @return mixed false, Title object of local target, or string with URL + * Tell the page view functions that this view was redirected + * from another page on the wiki. + * @param $from Title object. */ - public function getRedirectURL( $rt ) { - if ( $rt ) { - if ( $rt->getInterwiki() != '' ) { - if ( $rt->isLocal() ) { - // Offsite wikis need an HTTP redirect. - // - // This can be hard to reverse and may produce loops, - // so they may be disabled in the site configuration. - $source = $this->mTitle->getFullURL( 'redirect=no' ); - return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) ); - } - } else { - if ( $rt->getNamespace() == NS_SPECIAL ) { - // Gotta handle redirects to special pages differently: - // Fill the HTTP response "Location" header and ignore - // the rest of the page we're on. - // - // This can be hard to reverse, so they may be disabled. - if ( $rt->isSpecial( 'Userlogout' ) ) { - // rolleyes - } else { - return $rt->getFullURL(); - } - } - - return $rt; - } - } - - // No or invalid redirect - return false; + public function setRedirectedFrom( Title $from ) { + $this->mRedirectedFrom = $from; } /** @@ -212,31 +143,22 @@ class Article { * @return Title object of this page */ public function getTitle() { - return $this->mTitle; + return $this->mPage->getTitle(); } /** * Clear the object - * FIXME: shouldn't this be public? + * @todo FIXME: Shouldn't this be public? * @private */ public function clear() { - $this->mDataLoaded = false; $this->mContentLoaded = false; - $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded $this->mRedirectedFrom = null; # Title object if set - $this->mRedirectTarget = null; # Title object if set - $this->mUserText = - $this->mTimestamp = $this->mComment = ''; - $this->mGoodAdjustment = $this->mTotalAdjustment = 0; - $this->mTouched = '19700101000000'; - $this->mForUpdate = false; - $this->mIsRedirect = false; $this->mRevIdFetched = 0; $this->mRedirectUrl = false; - $this->mLatest = false; - $this->mPreparedEdit = false; + + $this->mPage->clear(); } /** @@ -250,20 +172,18 @@ class Article { * @return Return the text of this revision */ public function getContent() { - global $wgUser, $wgContLang, $wgMessageCache; + global $wgUser; wfProfileIn( __METHOD__ ); - if ( $this->getID() === 0 ) { + if ( $this->mPage->getID() === 0 ) { # If this is a MediaWiki:x message, then load the messages # and return the message value for x. - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # If this is a system message, get the default text. - list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); - $text = wfMsgGetKey( $message, false, $lang, false ); - - if ( wfEmptyMsg( $message, $text ) ) + if ( $this->getTitle()->getNamespace() == NS_MEDIAWIKI ) { + $text = $this->getTitle()->getDefaultMessageText(); + if ( $text === false ) { $text = ''; + } } else { $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); } @@ -278,71 +198,6 @@ class Article { } } - /** - * Get the text of the current revision. No side-effects... - * - * @return Return the text of the current revision - */ - public function getRawText() { - // Check process cache for current revision - if ( $this->mContentLoaded && $this->mOldId == 0 ) { - return $this->mContent; - } - - $rev = Revision::newFromTitle( $this->mTitle ); - $text = $rev ? $rev->getRawText() : false; - - return $text; - } - - /** - * This function returns the text of a section, specified by a number ($section). - * A section is text under a heading like == Heading == or \Heading\, or - * the first section before any such heading (section 0). - * - * If a section contains subsections, these are also returned. - * - * @param $text String: text to look in - * @param $section Integer: section number - * @return string text of the requested section - * @deprecated - */ - public function getSection( $text, $section ) { - global $wgParser; - return $wgParser->getSection( $text, $section ); - } - - /** - * Get the text that needs to be saved in order to undo all revisions - * between $undo and $undoafter. Revisions must belong to the same page, - * must exist and must not be deleted - * @param $undo Revision - * @param $undoafter Revision Must be an earlier revision than $undo - * @return mixed string on success, false on failure - */ - public function getUndoText( Revision $undo, Revision $undoafter = null ) { - $currentRev = Revision::newFromTitle( $this->mTitle ); - if ( !$currentRev ) { - return false; // no page - } - $undo_text = $undo->getText(); - $undoafter_text = $undoafter->getText(); - $cur_text = $currentRev->getText(); - - if ( $cur_text == $undo_text ) { - # No use doing a merge if it's just a straight revert. - return $undoafter_text; - } - - $undone_text = ''; - - if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) { - return false; - } - - return $undone_text; - } - /** * @return int The oldid of the article that is to be shown, 0 for the * current revision @@ -370,14 +225,14 @@ class Article { if ( isset( $oldid ) ) { $oldid = intval( $oldid ); if ( $wgRequest->getVal( 'direction' ) == 'next' ) { - $nextid = $this->mTitle->getNextRevisionID( $oldid ); + $nextid = $this->getTitle()->getNextRevisionID( $oldid ); if ( $nextid ) { $oldid = $nextid; } else { - $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' ); + $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' ); } } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) { - $previd = $this->mTitle->getPreviousRevisionID( $oldid ); + $previd = $this->getTitle()->getPreviousRevisionID( $oldid ); if ( $previd ) { $oldid = $previd; } @@ -401,101 +256,11 @@ class Article { wfProfileIn( __METHOD__ ); - $oldid = $this->getOldID(); - $this->mOldId = $oldid; - $this->fetchContent( $oldid ); + $this->fetchContent( $this->getOldID() ); wfProfileOut( __METHOD__ ); } - /** - * Fetch a page record with the given conditions - * @param $dbr Database object - * @param $conditions Array - * @return mixed Database result resource, or false on failure - */ - protected function pageData( $dbr, $conditions ) { - $fields = array( - 'page_id', - 'page_namespace', - 'page_title', - 'page_restrictions', - 'page_counter', - 'page_is_redirect', - 'page_is_new', - 'page_random', - 'page_touched', - 'page_latest', - 'page_len', - ); - - wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) ); - - $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ ); - - wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) ); - - return $row; - } - - /** - * Fetch a page record matching the Title object's namespace and title - * using a sanitized title string - * - * @param $dbr Database object - * @param $title Title object - * @return mixed Database result resource, or false on failure - */ - public function pageDataFromTitle( $dbr, $title ) { - return $this->pageData( $dbr, array( - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ) ); - } - - /** - * Fetch a page record matching the requested ID - * - * @param $dbr Database - * @param $id Integer - */ - protected function pageDataFromId( $dbr, $id ) { - return $this->pageData( $dbr, array( 'page_id' => $id ) ); - } - - /** - * Set the general counter, title etc data loaded from - * some source. - * - * @param $data Database row object or "fromdb" - */ - public function loadPageData( $data = 'fromdb' ) { - if ( $data === 'fromdb' ) { - $dbr = wfGetDB( DB_MASTER ); - $data = $this->pageDataFromId( $dbr, $this->getId() ); - } - - $lc = LinkCache::singleton(); - - if ( $data ) { - $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect, $data->page_latest ); - - $this->mTitle->mArticleID = intval( $data->page_id ); - - # Old-fashioned restrictions - $this->mTitle->loadRestrictions( $data->page_restrictions ); - - $this->mCounter = intval( $data->page_counter ); - $this->mTouched = wfTimestamp( TS_MW, $data->page_touched ); - $this->mIsRedirect = intval( $data->page_is_redirect ); - $this->mLatest = intval( $data->page_latest ); - } else { - $lc->addBadLinkObj( $this->mTitle ); - $this->mTitle->mArticleID = 0; - } - - $this->mDataLoaded = true; - } - /** * Get text of an article from database * Does *NOT* follow redirects. @@ -508,57 +273,44 @@ class Article { return $this->mContent; } - $dbr = wfGetDB( DB_MASTER ); - # Pre-fill content with error message so that if something # fails we'll have something telling us what we intended. - $t = $this->mTitle->getPrefixedText(); + $t = $this->getTitle()->getPrefixedText(); $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : ''; $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ; if ( $oldid ) { $revision = Revision::newFromId( $oldid ); - if ( $revision === null ) { + if ( !$revision ) { wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" ); return false; } - - $data = $this->pageDataFromId( $dbr, $revision->getPage() ); - - if ( !$data ) { - wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" ); - return false; - } - - $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title ); - $this->loadPageData( $data ); - } else { - if ( !$this->mDataLoaded ) { - $data = $this->pageDataFromTitle( $dbr, $this->mTitle ); - - if ( !$data ) { - wfDebug( __METHOD__ . " failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" ); + // Revision title doesn't match the page title given? + if ( $this->mPage->getID() != $revision->getPage() ) { + $function = array( get_class( $this->mPage ), 'newFromID' ); + $this->mPage = call_user_func( $function, $revision->getPage() ); + if ( !$this->mPage->getId() ) { + wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" ); return false; } - - $this->loadPageData( $data ); } - $revision = Revision::newFromId( $this->mLatest ); - if ( $revision === null ) { - wfDebug( __METHOD__ . " failed to retrieve current page, rev_id {$this->mLatest}\n" ); + } else { + if ( !$this->mPage->getLatest() ) { + wfDebug( __METHOD__ . " failed to find page data for title " . $this->getTitle()->getPrefixedText() . "\n" ); + return false; + } + + $revision = $this->mPage->getRevision(); + if ( !$revision ) { + wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" ); return false; } } - // FIXME: Horrible, horrible! This content-loading interface just plain sucks. + // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks. // We should instead work with the Revision object when we need it... $this->mContent = $revision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed - $this->mUser = $revision->getUser(); - $this->mUserText = $revision->getUserText(); - $this->mComment = $revision->getComment(); - $this->mTimestamp = wfTimestamp( TS_MW, $revision->getTimestamp() ); - $this->mRevIdFetched = $revision->getId(); $this->mContentLoaded = true; $this->mRevision =& $revision; @@ -569,118 +321,11 @@ class Article { } /** - * Read/write accessor to select FOR UPDATE - * - * @param $x Mixed: FIXME - * @return mixed value of $x, or value stored in Article::mForUpdate - */ - public function forUpdate( $x = null ) { - return wfSetVar( $this->mForUpdate, $x ); - } - - /** - * Get options for all SELECT statements - * - * @param $options Array: an optional options array which'll be appended to - * the default - * @return Array: options - */ - protected function getSelectOptions( $options = '' ) { - if ( $this->mForUpdate ) { - if ( is_array( $options ) ) { - $options[] = 'FOR UPDATE'; - } else { - $options = 'FOR UPDATE'; - } - } - - return $options; - } - - /** - * @return int Page ID - */ - public function getID() { - return $this->mTitle->getArticleID(); - } - - /** - * @return bool Whether or not the page exists in the database - */ - public function exists() { - return $this->getId() > 0; - } - - /** - * Check if this page is something we're going to be showing - * some sort of sensible content for. If we return false, page - * views (plain action=view) will return an HTTP 404 response, - * so spiders and robots can know they're following a bad link. - * - * @return bool - */ - public function hasViewableContent() { - return $this->exists() || $this->mTitle->isAlwaysKnown(); - } - - /** - * @return int The view count for the page - */ - public function getCount() { - if ( -1 == $this->mCounter ) { - $id = $this->getID(); - - if ( $id == 0 ) { - $this->mCounter = 0; - } else { - $dbr = wfGetDB( DB_SLAVE ); - $this->mCounter = $dbr->selectField( 'page', - 'page_counter', - array( 'page_id' => $id ), - __METHOD__, - $this->getSelectOptions() - ); - } - } - - return $this->mCounter; - } - - /** - * Determine whether a page would be suitable for being counted as an - * article in the site_stats table based on the title & its content - * - * @param $text String: text to analyze - * @return bool - */ - public function isCountable( $text ) { - global $wgUseCommaCount; - - $token = $wgUseCommaCount ? ',' : '[['; - - return $this->mTitle->isContentPage() && !$this->isRedirect( $text ) && in_string( $token, $text ); - } - - /** - * Tests if the article text represents a redirect - * - * @param $text mixed string containing article contents, or boolean - * @return bool + * No-op + * @deprecated since 1.18 */ - public function isRedirect( $text = false ) { - if ( $text === false ) { - if ( $this->mDataLoaded ) { - return $this->mIsRedirect; - } - - // Apparently loadPageData was never called - $this->loadContent(); - $titleObj = Title::newFromRedirectRecurse( $this->fetchContent() ); - } else { - $titleObj = Title::newFromRedirect( $text ); - } - - return $titleObj !== null; + public function forUpdate() { + wfDeprecated( __METHOD__ ); } /** @@ -694,80 +339,7 @@ class Article { return true; } - return $this->exists() && isset( $this->mRevision ) && $this->mRevision->isCurrent(); - } - - /** - * Loads everything except the text - * This isn't necessary for all uses, so it's only done if needed. - */ - protected function loadLastEdit() { - if ( -1 != $this->mUser ) { - return; - } - - # New or non-existent articles have no user information - $id = $this->getID(); - if ( 0 == $id ) { - return; - } - - $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id ); - if ( !is_null( $this->mLastRevision ) ) { - $this->mUser = $this->mLastRevision->getUser(); - $this->mUserText = $this->mLastRevision->getUserText(); - $this->mTimestamp = $this->mLastRevision->getTimestamp(); - $this->mComment = $this->mLastRevision->getComment(); - $this->mMinorEdit = $this->mLastRevision->isMinor(); - $this->mRevIdFetched = $this->mLastRevision->getId(); - } - } - - /** - * @return string GMT timestamp of last article revision - **/ - - public function getTimestamp() { - // Check if the field has been filled by ParserCache::get() - if ( !$this->mTimestamp ) { - $this->loadLastEdit(); - } - - return wfTimestamp( TS_MW, $this->mTimestamp ); - } - - /** - * @return int user ID for the user that made the last article revision - */ - public function getUser() { - $this->loadLastEdit(); - return $this->mUser; - } - - /** - * @return string username of the user that made the last article revision - */ - public function getUserText() { - $this->loadLastEdit(); - return $this->mUserText; - } - - /** - * @return string Comment stored for the last article revision - */ - public function getComment() { - $this->loadLastEdit(); - return $this->mComment; - } - - /** - * Returns true if last revision was marked as "minor edit" - * - * @return boolean Minor edit indicator for the last article revision. - */ - public function getMinorEdit() { - $this->loadLastEdit(); - return $this->mMinorEdit; + return $this->mPage->exists() && $this->mRevision && $this->mRevision->isCurrent(); } /** @@ -776,52 +348,11 @@ class Article { * @return int revision ID of last article revision */ public function getRevIdFetched() { - $this->loadLastEdit(); - return $this->mRevIdFetched; - } - - /** - * FIXME: this does what? - * @param $limit Integer: default 0. - * @param $offset Integer: default 0. - * @return UserArrayFromResult object with User objects of article contributors for requested range - */ - public function getContributors( $limit = 0, $offset = 0 ) { - # FIXME: this is expensive; cache this info somewhere. - - $dbr = wfGetDB( DB_SLAVE ); - $revTable = $dbr->tableName( 'revision' ); - $userTable = $dbr->tableName( 'user' ); - - $pageId = $this->getId(); - - $user = $this->getUser(); - - if ( $user ) { - $excludeCond = "AND rev_user != $user"; + if ( $this->mRevIdFetched ) { + return $this->mRevIdFetched; } else { - $userText = $dbr->addQuotes( $this->getUserText() ); - $excludeCond = "AND rev_user_text != $userText"; - } - - $deletedBit = $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ); // username hidden? - - $sql = "SELECT {$userTable}.*, rev_user_text as user_name, MAX(rev_timestamp) as timestamp - FROM $revTable LEFT JOIN $userTable ON rev_user = user_id - WHERE rev_page = $pageId - $excludeCond - AND $deletedBit = 0 - GROUP BY rev_user, rev_user_text - ORDER BY timestamp DESC"; - - if ( $limit > 0 ) { - $sql = $dbr->limitResult( $sql, $limit, $offset ); + return $this->mPage->getLatest(); } - - $sql .= ' ' . $this->getSelectOptions(); - $res = $dbr->query( $sql, __METHOD__ ); - - return new UserArrayFromResult( $res ); } /** @@ -836,67 +367,68 @@ class Article { # Get variables from query string $oldid = $this->getOldID(); - $parserCache = ParserCache::singleton(); - $parserOptions = $this->getParserOptions(); + # getOldID may want us to redirect somewhere else + if ( $this->mRedirectUrl ) { + $wgOut->redirect( $this->mRedirectUrl ); + wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); + wfProfileOut( __METHOD__ ); + + return; + } + + $wgOut->setArticleFlag( true ); + # Set page title (may be overridden by DISPLAYTITLE) + $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() ); + + # If we got diff in the query, we want to see a diff page instead of the article. + if ( $wgRequest->getCheck( 'diff' ) ) { + wfDebug( __METHOD__ . ": showing diff page\n" ); + $this->showDiffPage(); + wfProfileOut( __METHOD__ ); + + return; + } + + # Allow frames by default + $wgOut->allowClickjacking(); + + $parserCache = ParserCache::singleton(); + + $parserOptions = $this->mPage->getParserOptions(); # Render printable version, use printable version cache if ( $wgOut->isPrintable() ) { $parserOptions->setIsPrintable( true ); $parserOptions->setEditSection( false ); - } else if ( $wgUseETag && !$this->mTitle->quickUserCan( 'edit' ) ) { + } elseif ( $wgUseETag && !$this->getTitle()->quickUserCan( 'edit' ) ) { $parserOptions->setEditSection( false ); } # Try client and file cache - if ( $oldid === 0 && $this->checkTouched() ) { + if ( $oldid === 0 && $this->mPage->checkTouched() ) { if ( $wgUseETag ) { $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) ); } # Is it client cached? - if ( $wgOut->checkLastModified( $this->getTouched() ) ) { + if ( $wgOut->checkLastModified( $this->mPage->getTouched() ) ) { wfDebug( __METHOD__ . ": done 304\n" ); wfProfileOut( __METHOD__ ); return; # Try file cache - } else if ( $wgUseFileCache && $this->tryFileCache() ) { + } elseif ( $wgUseFileCache && $this->tryFileCache() ) { wfDebug( __METHOD__ . ": done file cache\n" ); # tell wgOut that output is taken care of $wgOut->disable(); - $this->viewUpdates(); + $this->mPage->viewUpdates(); wfProfileOut( __METHOD__ ); return; } } - # getOldID may want us to redirect somewhere else - if ( $this->mRedirectUrl ) { - $wgOut->redirect( $this->mRedirectUrl ); - wfDebug( __METHOD__ . ": redirecting due to oldid\n" ); - wfProfileOut( __METHOD__ ); - - return; - } - - $wgOut->setArticleFlag( true ); - # Set page title (may be overridden by DISPLAYTITLE) - $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - - # If we got diff in the query, we want to see a diff page instead of the article. - if ( $wgRequest->getCheck( 'diff' ) ) { - wfDebug( __METHOD__ . ": showing diff page\n" ); - $this->showDiffPage(); - wfProfileOut( __METHOD__ ); - - return; - } - - # Allow frames by default - $wgOut->allowClickjacking(); - - if ( !$wgUseETag && !$this->mTitle->quickUserCan( 'edit' ) ) { + if ( !$wgUseETag && !$this->getTitle()->quickUserCan( 'edit' ) ) { $parserOptions->setEditSection( false ); } @@ -931,14 +463,18 @@ class Article { $wgOut->addParserOutput( $this->mParserOutput ); # Ensure that UI elements requiring revision ID have # the correct version information. - $wgOut->setRevisionId( $this->mLatest ); + $wgOut->setRevisionId( $this->mPage->getLatest() ); $outputDone = true; + # Preload timestamp to avoid a DB hit + if ( isset( $this->mParserOutput->mTimestamp ) ) { + $this->mPage->setTimestamp( $this->mParserOutput->mTimestamp ); + } } } break; case 3: $text = $this->getContent(); - if ( $text === false || $this->getID() == 0 ) { + if ( $text === false || $this->mPage->getID() == 0 ) { wfDebug( __METHOD__ . ": showing missing article\n" ); $this->showMissingArticle(); wfProfileOut( __METHOD__ ); @@ -946,7 +482,7 @@ class Article { } # Another whitelist check in case oldid is altering the title - if ( !$this->mTitle->userCanRead() ) { + if ( !$this->getTitle()->userCanRead() ) { wfDebug( __METHOD__ . ": denied on secondary read check\n" ); $wgOut->loginToUse(); $wgOut->output(); @@ -966,14 +502,14 @@ class Article { } # If this "old" version is the current, then try the parser cache... - if ( $oldid === $this->getLatest() && $this->useParserCache( false ) ) { + if ( $oldid === $this->mPage->getLatest() && $this->useParserCache( false ) ) { $this->mParserOutput = $parserCache->get( $this, $parserOptions ); if ( $this->mParserOutput ) { - wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" ); + wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" ); $wgOut->addParserOutput( $this->mParserOutput ); - $wgOut->setRevisionId( $this->mLatest ); + $wgOut->setRevisionId( $this->mPage->getLatest() ); $outputDone = true; - break; + break; } } } @@ -983,7 +519,7 @@ class Article { $wgOut->setRevisionId( $this->getRevIdFetched() ); # Pages containing custom CSS or JavaScript get special treatment - if ( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) { wfDebug( __METHOD__ . ": showing CSS/JS source\n" ); $this->showCssOrJsPage(); $outputDone = true; @@ -995,7 +531,7 @@ class Article { # Don't append the subtitle if this was an old revision $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); # Parse just to get categories, displaytitle, etc. - $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, $parserOptions ); + $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions ); $wgOut->addParserOutputNoText( $this->mParserOutput ); $outputDone = true; } @@ -1007,7 +543,7 @@ class Article { $key = $parserCache->getKey( $this, $parserOptions ); $poolArticleView = new PoolWorkArticleView( $this, $key, $useParserCache, $parserOptions ); - + if ( !$poolArticleView->execute() ) { # Connection or timeout error wfProfileOut( __METHOD__ ); @@ -1035,10 +571,11 @@ class Article { # tents of 'pagetitle-view-mainpage' instead of the default (if # that's not empty). # This message always exists because it is in the i18n files - if ( $this->mTitle->equals( Title::newMainPage() ) - && ( $m = wfMsgForContent( 'pagetitle-view-mainpage' ) ) !== '' ) - { - $wgOut->setHTMLTitle( $m ); + if ( $this->getTitle()->equals( Title::newMainPage() ) ) { + $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage(); + if ( !$msg->isDisabled() ) { + $wgOut->setHTMLTitle( $msg->title( $this->getTitle() )->text() ); + } } # Now that we've filled $this->mParserOutput, we know whether @@ -1048,7 +585,7 @@ class Article { $wgOut->setFollowPolicy( $policy['follow'] ); $this->showViewFooter(); - $this->viewUpdates(); + $this->mPage->viewUpdates(); wfProfileOut( __METHOD__ ); } @@ -1066,16 +603,14 @@ class Article { $unhide = $wgRequest->getInt( 'unhide' ) == 1; $oldid = $this->getOldID(); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $unhide ); + $de = new DifferenceEngine( $this->getTitle(), $oldid, $diff, $rcid, $purge, $unhide ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); - // Needed to get the page's current revision - $this->loadPageData(); - if ( $diff == 0 || $diff == $this->mLatest ) { + if ( $diff == 0 || $diff == $this->mPage->getLatest() ) { # Run view updates for current revision only - $this->viewUpdates(); + $this->mPage->viewUpdates(); } } @@ -1087,15 +622,19 @@ class Article { * page views. */ protected function showCssOrJsPage() { - global $wgOut; + global $wgOut, $wgLang; - $wgOut->wrapWikiMsg( "
\n$1\n
", 'clearyourcache' ); + $dir = $wgLang->getDir(); + $lang = $wgLang->getCode(); + + $wgOut->wrapWikiMsg( "
\n$1\n
", + 'clearyourcache' ); // Give hooks a chance to customise the output - if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { + if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) { // Wrap the whole lot in a
 and don't parse
 			$m = array();
-			preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+			preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 			$wgOut->addHTML( "
\n" );
 			$wgOut->addHTML( htmlspecialchars( $this->mContent ) );
 			$wgOut->addHTML( "\n
\n" ); @@ -1112,13 +651,12 @@ class Article { global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies; global $wgDefaultRobotPolicy, $wgRequest; - $ns = $this->mTitle->getNamespace(); + $ns = $this->getTitle()->getNamespace(); if ( $ns == NS_USER || $ns == NS_USER_TALK ) { # Don't index user and user talk pages for blocked users (bug 11443) - if ( !$this->mTitle->isSubpage() ) { - $block = new Block(); - if ( $block->load( $this->mTitle->getText() ) ) { + if ( !$this->getTitle()->isSubpage() ) { + if ( Block::newFromTarget( null, $this->getTitle()->getText() ) instanceof Block ) { return array( 'index' => 'noindex', 'follow' => 'nofollow' @@ -1127,7 +665,7 @@ class Article { } } - if ( $this->getID() === 0 || $this->getOldID() ) { + if ( $this->mPage->getID() === 0 || $this->getOldID() ) { # Non-articles (special pages etc), and old revisions return array( 'index' => 'noindex', @@ -1157,7 +695,7 @@ class Article { self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) ); } - if ( $this->mTitle->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) { + if ( $this->getTitle()->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) { # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates # a final sanity check that we have really got the parser output. $policy = array_merge( @@ -1166,11 +704,11 @@ class Article { ); } - if ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) { + if ( isset( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) { # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__ $policy = array_merge( $policy, - self::formatRobotPolicy( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) + self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ); } @@ -1182,7 +720,7 @@ class Article { * merging of several policies using array_merge(). * @param $policy Mixed, returns empty array on null/false/'', transparent * to already-converted arrays, converts String. - * @return associative Array: 'index' => , 'follow' => + * @return Array: 'index' => , 'follow' => */ public static function formatRobotPolicy( $policy ) { if ( is_array( $policy ) ) { @@ -1214,16 +752,15 @@ class Article { * @return boolean */ public function showRedirectedFromHeader() { - global $wgOut, $wgUser, $wgRequest, $wgRedirectSources; + global $wgOut, $wgRequest, $wgRedirectSources; $rdfrom = $wgRequest->getVal( 'rdfrom' ); - $sk = $wgUser->getSkin(); if ( isset( $this->mRedirectedFrom ) ) { // This is an internally redirected page view. // We'll need a backlink to the source page for navigation. if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) { - $redir = $sk->link( + $redir = Linker::link( $this->mRedirectedFrom, null, array(), @@ -1235,14 +772,14 @@ class Article { $wgOut->setSubtitle( $s ); // Set the fragment if one was specified in the redirect - if ( strval( $this->mTitle->getFragment() ) != '' ) { - $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); + if ( strval( $this->getTitle()->getFragment() ) != '' ) { + $fragment = Xml::escapeJsString( $this->getTitle()->getFragmentForURL() ); $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); } // Add a tag $wgOut->addLink( array( 'rel' => 'canonical', - 'href' => $this->mTitle->getLocalURL() ) + 'href' => $this->getTitle()->getLocalURL() ) ); return true; @@ -1251,7 +788,7 @@ class Article { // This is an externally redirected view, from some other wiki. // If it was reported from a trusted site, supply a backlink. if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) { - $redir = $sk->makeExternalLink( $rdfrom, $rdfrom ); + $redir = Linker::makeExternalLink( $rdfrom, $rdfrom ); $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir ); $wgOut->setSubtitle( $s ); @@ -1269,9 +806,8 @@ class Article { public function showNamespaceHeader() { global $wgOut; - if ( $this->mTitle->isTalkPage() ) { - $msg = wfMsgNoTrans( 'talkpageheader' ); - if ( $msg !== '-' && !wfEmptyMsg( 'talkpageheader', $msg ) ) { + if ( $this->getTitle()->isTalkPage() ) { + if ( !wfMessage( 'talkpageheader' )->isDisabled() ) { $wgOut->wrapWikiMsg( "
\n$1\n
", array( 'talkpageheader' ) ); } } @@ -1284,7 +820,7 @@ class Article { global $wgOut, $wgUseTrackbacks; # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page - if ( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) { + if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) { $wgOut->addWikiMsg( 'anontalkpagetext' ); } @@ -1296,6 +832,9 @@ class Article { if ( $wgUseTrackbacks ) { $this->addTrackbacks(); } + + wfRunHooks( 'ArticleViewFooter', array( $this ) ); + } /** @@ -1308,11 +847,10 @@ class Article { $rcid = $wgRequest->getVal( 'rcid' ); - if ( !$rcid || !$this->mTitle->quickUserCan( 'patrol' ) ) { + if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol' ) ) { return; } - $sk = $wgUser->getSkin(); $token = $wgUser->editToken( $rcid ); $wgOut->preventClickjacking(); @@ -1320,8 +858,8 @@ class Article { "'; + wfProfileOut( __METHOD__ ); return $wgContLang->convert( $r ); } @@ -160,14 +211,6 @@ class CategoryViewer { } } - function getSkin() { - if ( !$this->skin ) { - global $wgUser; - $this->skin = $wgUser->getSkin(); - } - return $this->skin; - } - /** * Add a subcategory to the internal lists, using a Category object */ @@ -175,7 +218,7 @@ class CategoryViewer { // Subcategory; strip the 'Category' namespace from the link text. $title = $cat->getTitle(); - $link = $this->getSkin()->link( $title, $title->getText() ); + $link = Linker::link( $title, htmlspecialchars( $title->getText() ) ); if ( $title->isRedirect() ) { // This didn't used to add redirect-in-category, but might // as well be consistent with the rest of the sections @@ -190,7 +233,7 @@ class CategoryViewer { /** * Add a subcategory to the internal lists, using a title object - * @deprecated kept for compatibility, please use addSubcategoryObject instead + * @deprecated since 1.17 kept for compatibility, please use addSubcategoryObject instead */ function addSubcategory( Title $title, $sortkey, $pageLength ) { $this->addSubcategoryObject( Category::newFromTitle( $title ), $sortkey, $pageLength ); @@ -233,7 +276,7 @@ class CategoryViewer { $this->gallery->add( $title ); } } else { - $link = $this->getSkin()->link( $title ); + $link = Linker::link( $title ); if ( $isRedirect ) { // This seems kind of pointless given 'mw-redirect' class, // but keeping for back-compatibility with user css. @@ -252,7 +295,7 @@ class CategoryViewer { function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { global $wgContLang; - $link = $this->getSkin()->link( $title ); + $link = Linker::link( $title ); if ( $isRedirect ) { // This seems kind of pointless given 'mw-redirect' class, // but keeping for back-compatiability with user css. @@ -309,7 +352,7 @@ class CategoryViewer { 'page_is_redirect', 'cl_sortkey', 'cat_id', 'cat_title', 'cat_subcats', 'cat_pages', 'cat_files', 'cl_sortkey_prefix', 'cl_collation' ), - array( 'cl_to' => $this->title->getDBkey() ) + $extraConds, + array_merge( array( 'cl_to' => $this->title->getDBkey() ), $extraConds ), __METHOD__, array( 'USE INDEX' => array( 'categorylinks' => 'cl_sortkey' ), @@ -385,7 +428,7 @@ class CategoryViewer { # Don't show articles section if there are none. $r = ''; - # FIXME, here and in the other two sections: we don't need to bother + # @todo FIXME: Here and in the other two sections: we don't need to bother # with this rigamarole if the entire category contents fit on one page # and have already been retrieved. We can just use $rescnt in that # case and save a query and some logic. @@ -460,13 +503,20 @@ class CategoryViewer { * @private */ function formatList( $articles, $articles_start_char, $cutoff = 6 ) { + $list = ''; if ( count ( $articles ) > $cutoff ) { - return self::columnList( $articles, $articles_start_char ); + $list = self::columnList( $articles, $articles_start_char ); } elseif ( count( $articles ) > 0 ) { // for short lists of articles in categories. - return self::shortList( $articles, $articles_start_char ); + $list = self::shortList( $articles, $articles_start_char ); } - return ''; + + $pageLang = $this->title->getPageLanguage(); + $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), + 'class' => 'mw-content-'.$pageLang->getDir() ); + $list = Html::rawElement( 'div', $attribs, $list ); + + return $list; } /** @@ -539,10 +589,8 @@ class CategoryViewer { static function shortList( $articles, $articles_start_char ) { $r = '

' . htmlspecialchars( $articles_start_char[0] ) . "

\n"; $r .= '
  • ' . $articles[0] . '
  • '; - for ( $index = 1; $index < count( $articles ); $index++ ) - { - if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) - { + for ( $index = 1; $index < count( $articles ); $index++ ) { + if ( $articles_start_char[$index] != $articles_start_char[$index - 1] ) { $r .= "

" . htmlspecialchars( $articles_start_char[$index] ) . "

\n
    "; } @@ -563,7 +611,7 @@ class CategoryViewer { */ private function pagingLinks( $first, $last, $type = '' ) { global $wgLang; - $sk = $this->getSkin(); + $limitText = $wgLang->formatNum( $this->limit ); $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText ); @@ -572,8 +620,8 @@ class CategoryViewer { $prevQuery = $this->query; $prevQuery["{$type}until"] = $first; unset( $prevQuery["{$type}from"] ); - $prevLink = $sk->linkKnown( - $this->title, + $prevLink = Linker::linkKnown( + $this->addFragmentToTitle( $this->title, $type ), $prevLink, array(), $prevQuery @@ -586,8 +634,8 @@ class CategoryViewer { $lastQuery = $this->query; $lastQuery["{$type}from"] = $last; unset( $lastQuery["{$type}until"] ); - $nextLink = $sk->linkKnown( - $this->title, + $nextLink = Linker::linkKnown( + $this->addFragmentToTitle( $this->title, $type ), $nextLink, array(), $lastQuery @@ -597,9 +645,35 @@ class CategoryViewer { return "($prevLink) ($nextLink)"; } + /** + * Takes a title, and adds the fragment identifier that + * corresponds to the correct segment of the category. + * + * @param Title $title: The title (usually $this->title) + * @param String $section: Which section + */ + private function addFragmentToTitle( $title, $section ) { + switch ( $section ) { + case 'page': + $fragment = 'mw-pages'; + break; + case 'subcat': + $fragment = 'mw-subcategories'; + break; + case 'file': + $fragment = 'mw-category-media'; + break; + default: + throw new MWException( __METHOD__ . + " Invalid section $section." ); + } + + return Title::makeTitle( $title->getNamespace(), + $title->getDBkey(), $fragment ); + } /** * What to do if the category table conflicts with the number of results - * returned? This function says what. Each type is considered independantly + * returned? This function says what. Each type is considered independently * of the other types. * * Note for grepping: uses the messages category-article-count, @@ -640,8 +714,7 @@ class CategoryViewer { } if ( $dbcnt == $rescnt || ( ( $rescnt == $this->limit || $fromOrUntil ) - && $dbcnt > $rescnt ) ) - { + && $dbcnt > $rescnt ) ) { # Case 1: seems sane. $totalcnt = $dbcnt; } elseif ( $rescnt < $this->limit && !$fromOrUntil ) { diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index 1f08b7f8..2567de0d 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -42,6 +42,7 @@ class Categoryfinder { * @param $article_ids Array of article IDs * @param $categories FIXME * @param $mode String: FIXME, default 'AND'. + * @todo FIXME: $categories/$mode */ function seed( $article_ids, $categories, $mode = 'AND' ) { $this->articles = $article_ids; @@ -85,9 +86,9 @@ class Categoryfinder { /** * This functions recurses through the parent representation, trying to match the conditions - * @param $id The article/category to check - * @param $conds The array of categories to match - * @param $path used to check for recursion loops + * @param $id int The article/category to check + * @param $conds array The array of categories to match + * @param $path array used to check for recursion loops * @return bool Does this match the conditions? */ function check( $id, &$conds, $path = array() ) { diff --git a/includes/Cdb.php b/includes/Cdb.php index 60477485..d7a2bca5 100644 --- a/includes/Cdb.php +++ b/includes/Cdb.php @@ -13,6 +13,10 @@ abstract class CdbReader { /** * Open a file and return a subclass instance + * + * @param $fileName string + * + * @return CdbReader */ public static function open( $fileName ) { if ( self::haveExtension() ) { @@ -25,6 +29,8 @@ abstract class CdbReader { /** * Returns true if the native extension is available + * + * @return bool */ public static function haveExtension() { if ( !function_exists( 'dba_handlers' ) ) { @@ -49,6 +55,8 @@ abstract class CdbReader { /** * Get a value with a given key. Only string values are supported. + * + * @param $key string */ abstract public function get( $key ); } @@ -61,6 +69,10 @@ abstract class CdbWriter { /** * Open a writer and return a subclass instance. * The user must have write access to the directory, for temporary file creation. + * + * @param $fileName string + * + * @return bool */ public static function open( $fileName ) { if ( CdbReader::haveExtension() ) { diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php index 1485cc66..f4029ba5 100644 --- a/includes/Cdb_PHP.php +++ b/includes/Cdb_PHP.php @@ -16,6 +16,11 @@ class CdbFunctions { /** * Take a modulo of a signed integer as if it were an unsigned integer. * $b must be less than 0x40000000 and greater than 0 + * + * @param $a + * @param $b + * + * @return int */ public static function unsignedMod( $a, $b ) { if ( $a & 0x80000000 ) { @@ -25,9 +30,12 @@ class CdbFunctions { return $a % $b; } } - + /** * Shift a signed integer right as if it were unsigned + * @param $a + * @param $b + * @return int */ public static function unsignedShiftRight( $a, $b ) { if ( $b == 0 ) { @@ -42,6 +50,10 @@ class CdbFunctions { /** * The CDB hash function. + * + * @param $s + * + * @return */ public static function hash( $s ) { $h = 5381; @@ -103,11 +115,16 @@ class CdbReader_PHP extends CdbReader { } function close() { - if( isset($this->handle) ) + if( isset( $this->handle ) ) { fclose( $this->handle ); + } unset( $this->handle ); } + /** + * @param $key + * @return bool|string + */ public function get( $key ) { // strval is required if ( $this->find( strval( $key ) ) ) { @@ -117,6 +134,11 @@ class CdbReader_PHP extends CdbReader { } } + /** + * @param $key + * @param $pos + * @return bool + */ protected function match( $key, $pos ) { $buf = $this->read( strlen( $key ), $pos ); return $buf === $key; @@ -126,6 +148,12 @@ class CdbReader_PHP extends CdbReader { $this->loop = 0; } + /** + * @throws MWException + * @param $length + * @param $pos + * @return string + */ protected function read( $length, $pos ) { if ( fseek( $this->handle, $pos ) == -1 ) { // This can easily happen if the internal pointers are incorrect @@ -145,6 +173,8 @@ class CdbReader_PHP extends CdbReader { /** * Unpack an unsigned integer and throw an exception if it needs more than 31 bits + * @param $s + * @return */ protected function unpack31( $s ) { $data = unpack( 'V', $s ); @@ -156,12 +186,18 @@ class CdbReader_PHP extends CdbReader { /** * Unpack a 32-bit signed integer + * @param $s + * @return int */ protected function unpackSigned( $s ) { $data = unpack( 'va/vb', $s ); return $data['a'] | ( $data['b'] << 16 ); } + /** + * @param $key + * @return bool + */ protected function findNext( $key ) { if ( !$this->loop ) { $u = CdbFunctions::hash( $key ); @@ -204,6 +240,10 @@ class CdbReader_PHP extends CdbReader { return false; } + /** + * @param $key + * @return bool + */ protected function find( $key ) { $this->findStart(); return $this->findNext( $key ); @@ -240,6 +280,11 @@ class CdbWriter_PHP extends CdbWriter { } } + /** + * @param $key + * @param $value + * @return + */ public function set( $key, $value ) { if ( strval( $key ) === '' ) { // DBA cross-check hack @@ -251,10 +296,14 @@ class CdbWriter_PHP extends CdbWriter { $this->addend( strlen( $key ), strlen( $value ), CdbFunctions::hash( $key ) ); } + /** + * @throws MWException + */ public function close() { $this->finish(); - if( isset($this->handle) ) + if( isset($this->handle) ) { fclose( $this->handle ); + } if ( wfIsWindows() && file_exists($this->realFileName) ) { unlink( $this->realFileName ); } @@ -264,6 +313,10 @@ class CdbWriter_PHP extends CdbWriter { unset( $this->handle ); } + /** + * @throws MWException + * @param $buf + */ protected function write( $buf ) { $len = fwrite( $this->handle, $buf ); if ( $len !== strlen( $buf ) ) { @@ -271,6 +324,10 @@ class CdbWriter_PHP extends CdbWriter { } } + /** + * @throws MWException + * @param $len + */ protected function posplus( $len ) { $newpos = $this->pos + $len; if ( $newpos > 0x7fffffff ) { @@ -279,6 +336,11 @@ class CdbWriter_PHP extends CdbWriter { $this->pos = $newpos; } + /** + * @param $keylen + * @param $datalen + * @param $h + */ protected function addend( $keylen, $datalen, $h ) { $this->hplist[] = array( 'h' => $h, @@ -291,6 +353,11 @@ class CdbWriter_PHP extends CdbWriter { $this->posplus( $datalen ); } + /** + * @throws MWException + * @param $keylen + * @param $datalen + */ protected function addbegin( $keylen, $datalen ) { if ( $keylen > 0x7fffffff ) { throw new MWException( __METHOD__.': key length too long' ); @@ -302,6 +369,9 @@ class CdbWriter_PHP extends CdbWriter { $this->write( $buf ); } + /** + * @throws MWException + */ protected function finish() { // Hack for DBA cross-check $this->hplist = array_reverse( $this->hplist ); diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php index 7f0fee21..c8e522df 100644 --- a/includes/ChangeTags.php +++ b/includes/ChangeTags.php @@ -1,8 +1,5 @@ exists() ? $msg->parse() : htmlspecialchars( $tag ); } ## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id. @@ -150,18 +144,26 @@ class ChangeTags { } /** - * If $fullForm is set to false, then it returns an array of (label, form). - * If $fullForm is true, it returns an entire form. + * Build a text box to select a change tag + * + * @param $selected String: tag to select by default + * @param $fullForm Boolean: + * - if false, then it returns an array of (label, form). + * - if true, it returns an entire form around the selector. + * @param $title Title object to send the form to. + * Used when, and only when $fullForm is true. + * @return String or array: + * - if $fullForm is false: Array with + * - if $fullForm is true: String, html fragment */ - static function buildTagFilterSelector( $selected='', $fullForm = false /* used to put a full form around the selector */ ) { + public static function buildTagFilterSelector( $selected='', $fullForm = false, Title $title = null ) { global $wgUseTagFilter; if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) return $fullForm ? '' : array(); - global $wgTitle; - - $data = array( wfMsgExt( 'tag-filter', 'parseinline' ), Xml::input( 'tagfilter', 20, $selected ) ); + $data = array( Html::rawElement( 'label', array( 'for' => 'tagfilter' ), wfMsgExt( 'tag-filter', 'parseinline' ) ), + Xml::input( 'tagfilter', 20, $selected ) ); if ( !$fullForm ) { return $data; @@ -175,7 +177,11 @@ class ChangeTags { return $html; } - /** Basically lists defined tags which count even if they aren't applied to anything */ + /** + *Basically lists defined tags which count even if they aren't applied to anything + * + * @return array + */ static function listDefinedTags() { // Caching... global $wgMemc; diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index f07b6505..c4c4a8a1 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -24,15 +24,19 @@ class ChangesFeed { * * @param $title String: feed's title * @param $description String: feed's description + * @param $url String: url of origin page * @return ChannelFeed subclass or false on failure */ - public function getFeedObject( $title, $description ) { - global $wgSitename, $wgLanguageCode, $wgFeedClasses, $wgTitle; - $feedTitle = "$wgSitename - {$title} [$wgLanguageCode]"; - if( !isset($wgFeedClasses[$this->format] ) ) + public function getFeedObject( $title, $description, $url ) { + global $wgSitename, $wgLanguageCode, $wgFeedClasses; + + if ( !isset( $wgFeedClasses[$this->format] ) ) { return false; + } + + $feedTitle = "$wgSitename - {$title} [$wgLanguageCode]"; return new $wgFeedClasses[$this->format]( - $feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() ); + $feedTitle, htmlspecialchars( $description ), $url ); } /** @@ -57,11 +61,11 @@ class ChangesFeed { FeedUtils::checkPurge( $timekey, $key ); - /* - * Bumping around loading up diffs can be pretty slow, so where - * possible we want to cache the feed output so the next visitor - * gets it quick too. - */ + /** + * Bumping around loading up diffs can be pretty slow, so where + * possible we want to cache the feed output so the next visitor + * gets it quick too. + */ $cachedFeed = $this->loadFromCache( $lastmod, $timekey, $key ); if( is_string( $cachedFeed ) ) { wfDebug( "RC: Outputting cached feed\n" ); @@ -106,12 +110,12 @@ class ChangesFeed { $feedLastmod = $messageMemc->get( $timekey ); if( ( $wgFeedCacheTimeout > 0 ) && $feedLastmod ) { - /* - * If the cached feed was rendered very recently, we may - * go ahead and use it even if there have been edits made - * since it was rendered. This keeps a swarm of requests - * from being too bad on a super-frequently edited wiki. - */ + /** + * If the cached feed was rendered very recently, we may + * go ahead and use it even if there have been edits made + * since it was rendered. This keeps a swarm of requests + * from being too bad on a super-frequently edited wiki. + */ $feedAge = time() - wfTimestamp( TS_UNIX, $feedLastmod ); $feedLastmodUnix = wfTimestamp( TS_UNIX, $feedLastmod ); @@ -145,6 +149,7 @@ class ChangesFeed { $n = 0; foreach( $rows as $obj ) { if( $n > 0 && + $obj->rc_type == RC_EDIT && $obj->rc_namespace >= 0 && $obj->rc_cur_id == $sorted[$n-1]->rc_cur_id && $obj->rc_user_text == $sorted[$n-1]->rc_user_text ) { @@ -157,16 +162,27 @@ class ChangesFeed { foreach( $sorted as $obj ) { $title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title ); - $talkpage = $title->getTalkPage(); + $talkpage = MWNamespace::canTalk( $obj->rc_namespace ) ? $title->getTalkPage()->getFullUrl() : ''; // Skip items with deleted content (avoids partially complete/inconsistent output) if( $obj->rc_deleted ) continue; + + if ( $obj->rc_this_oldid ) { + $url = $title->getFullURL( + 'diff=' . $obj->rc_this_oldid . + '&oldid=' . $obj->rc_last_oldid + ); + } else { + // log entry or something like that. + $url = $title->getFullURL(); + } + $item = new FeedItem( $title->getPrefixedText(), FeedUtils::formatDiff( $obj ), - $obj->rc_this_oldid ? $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ) : $title->getFullURL(), + $url, $obj->rc_timestamp, ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text, - $talkpage->getFullURL() + $talkpage ); $feed->outItem( $item ); } diff --git a/includes/ChangesList.php b/includes/ChangesList.php index b8bc4f55..1858dc3a 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -16,6 +16,10 @@ class RCCacheEntry extends RecentChange { var $curlink , $difflink, $lastlink, $usertalklink, $versionlink; var $userlink, $timestamp, $watched; + /** + * @param $rc RecentChange + * @return RCCacheEntry + */ static function newFromParent( $rc ) { $rc2 = new RCCacheEntry; $rc2->mAttribs = $rc->mAttribs; @@ -27,39 +31,64 @@ class RCCacheEntry extends RecentChange { /** * Base class for all changes lists */ -class ChangesList { +class ChangesList extends ContextSource { + + /** + * @var Skin + */ public $skin; + protected $watchlist = false; + protected $message; + /** - * Changeslist contructor - * @param $skin Skin - */ - public function __construct( $skin ) { - $this->skin = $skin; + * Changeslist contructor + * + * @param $obj Skin or IContextSource + */ + public function __construct( $obj ) { + if ( $obj instanceof IContextSource ) { + $this->setContext( $obj ); + $this->skin = $obj->getSkin(); + } else { + $this->setContext( $obj->getContext() ); + $this->skin = $obj; + } $this->preCacheMessages(); } /** - * Fetch an appropriate changes list class for the specified user - * Some users might want to use an enhanced list format, for instance + * Fetch an appropriate changes list class for the main context + * This first argument used to be an User object. * - * @param $user User to fetch the list class for - * @return ChangesList derivative + * @deprecated in 1.18; use newFromContext() instead + * @param $unused Unused + * @return ChangesList|EnhancedChangesList|OldChangesList derivative */ - public static function newFromUser( &$user ) { - global $wgRequest; + public static function newFromUser( $unused ) { + return self::newFromContext( RequestContext::getMain() ); + } - $sk = $user->getSkin(); + /** + * Fetch an appropriate changes list class for the specified context + * Some users might want to use an enhanced list format, for instance + * + * @param $context IContextSource to use + * @return ChangesList|EnhancedChangesList|OldChangesList derivative + */ + public static function newFromContext( IContextSource $context ) { + $user = $context->getUser(); + $sk = $context->getSkin(); $list = null; - if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) { - $new = $wgRequest->getBool( 'enhanced', $user->getOption( 'usenewrc' ) ); - return $new ? new EnhancedChangesList( $sk ) : new OldChangesList( $sk ); + if( wfRunHooks( 'FetchChangesList', array( $user, &$sk, &$list ) ) ) { + $new = $context->getRequest()->getBool( 'enhanced', $user->getOption( 'usenewrc' ) ); + return $new ? new EnhancedChangesList( $context ) : new OldChangesList( $context ); } else { return $list; } } - + /** * Sets the list to use a
  • tag * @param $value Boolean @@ -81,21 +110,19 @@ class ChangesList { } } - /** * Returns the appropriate flags for new page, minor change and patrolling - * @param $new Boolean - * @param $minor Boolean - * @param $patrolled Boolean + * @param $flags Array Associative array of 'flag' => Bool * @param $nothing String to use for empty space - * @param $bot Boolean * @return String */ - protected function recentChangesFlags( $new, $minor, $patrolled, $nothing = ' ', $bot = false ) { - $f = $new ? self::flag( 'newpage' ) : $nothing; - $f .= $minor ? self::flag( 'minor' ) : $nothing; - $f .= $bot ? self::flag( 'bot' ) : $nothing; - $f .= $patrolled ? self::flag( 'unpatrolled' ) : $nothing; + protected function recentChangesFlags( $flags, $nothing = ' ' ) { + $f = ''; + foreach( array( 'newpage', 'minor', 'bot', 'unpatrolled' ) as $flag ){ + $f .= isset( $flags[$flag] ) && $flags[$flag] + ? self::flag( $flag ) + : $nothing; + } return $f; } @@ -105,28 +132,36 @@ class ChangesList { * unpatrolled edit. By default in English it will contain "N", "m", "b", * "!" respectively, plus it will have an appropriate title and class. * - * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot' + * @param $flag String: 'newpage', 'unpatrolled', 'minor', or 'bot' * @return String: Raw HTML */ - public static function flag( $key ) { + public static function flag( $flag ) { static $messages = null; if ( is_null( $messages ) ) { - foreach ( explode( ' ', 'minoreditletter boteditletter newpageletter ' . - 'unpatrolledletter recentchanges-label-minor recentchanges-label-bot ' . - 'recentchanges-label-newpage recentchanges-label-unpatrolled' ) as $msg ) { - $messages[$msg] = wfMsgExt( $msg, 'escapenoentities' ); + $messages = array( + 'newpage' => array( 'newpageletter', 'recentchanges-label-newpage' ), + 'minoredit' => array( 'minoreditletter', 'recentchanges-label-minor' ), + 'botedit' => array( 'boteditletter', 'recentchanges-label-bot' ), + 'unpatrolled' => array( 'unpatrolledletter', 'recentchanges-label-unpatrolled' ), + ); + foreach( $messages as &$value ) { + $value[0] = wfMsgExt( $value[0], 'escapenoentities' ); + $value[1] = wfMsgExt( $value[1], 'escapenoentities' ); } } + # Inconsistent naming, bleh - if ( $key == 'newpage' || $key == 'unpatrolled' ) { - $key2 = $key; - } else { - $key2 = $key . 'edit'; - } - return "" - . $messages["${key2}letter"] - . ''; + $map = array( + 'newpage' => 'newpage', + 'minor' => 'minoredit', + 'bot' => 'botedit', + 'unpatrolled' => 'unpatrolled', + 'minoredit' => 'minoredit', + 'botedit' => 'botedit', + ); + $flag = $map[$flag]; + + return "" . $messages[$flag][0] . ''; } /** @@ -141,12 +176,12 @@ class ChangesList { $this->rclistOpen = false; return ''; } - + /** * Show formatted char difference * @param $old Integer: bytes * @param $new Integer: bytes - * @returns String + * @return String */ public static function showCharacterDifference( $old, $new ) { global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode; @@ -157,29 +192,29 @@ class ChangesList { if ( !isset($fastCharDiff[$code]) ) { $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1'; } - + $formatedSize = $wgLang->formatNum($szdiff); if ( !$fastCharDiff[$code] ) { $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $formatedSize ); } - + if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { $tag = 'strong'; } else { - $tag = 'span'; + $tag = 'span'; } if( $szdiff === 0 ) { return "<$tag class='mw-plusminus-null'>($formatedSize)"; } elseif( $szdiff > 0 ) { return "<$tag class='mw-plusminus-pos'>(+$formatedSize)"; - } else { + } else { return "<$tag class='mw-plusminus-neg'>($formatedSize)"; } } /** - * Returns text for the end of RC + * Returns text for the end of RC * @return String */ public function endRecentChangesList() { @@ -190,42 +225,38 @@ class ChangesList { } } + /** + * @param $s + * @param $rc RecentChange + * @return void + */ public function insertMove( &$s, $rc ) { # Diff $s .= '(' . $this->message['diff'] . ') ('; # Hist - $s .= $this->skin->link( + $s .= Linker::linkKnown( $rc->getMovedToTitle(), $this->message['hist'], array(), - array( 'action' => 'history' ), - array( 'known', 'noclasses' ) + array( 'action' => 'history' ) ) . ') . . '; # "[[x]] moved to [[y]]" $msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir'; - $s .= wfMsg( + $s .= wfMsgHtml( $msg, - $this->skin->link( + Linker::linkKnown( $rc->getTitle(), null, array(), - array( 'redirect' => 'no' ), - array( 'known', 'noclasses' ) + array( 'redirect' => 'no' ) ), - $this->skin->link( - $rc->getMovedToTitle(), - null, - array(), - array(), - array( 'known', 'noclasses' ) - ) + Linker::linkKnown( $rc->getMovedToTitle() ) ); } public function insertDateHeader( &$s, $rc_timestamp ) { - global $wgLang; # Make date header if necessary - $date = $wgLang->date( $rc_timestamp, true, true ); + $date = $this->getLang()->date( $rc_timestamp, true, true ); if( $date != $this->lastdate ) { if( $this->lastdate != '' ) { $s .= "
\n"; @@ -238,20 +269,20 @@ class ChangesList { public function insertLog( &$s, $title, $logtype ) { $logname = LogPage::logName( $logtype ); - $s .= '(' . $this->skin->link( - $title, - $logname, - array(), - array(), - array( 'known', 'noclasses' ) - ) . ')'; + $s .= '(' . Linker::linkKnown( $title, htmlspecialchars( $logname ) ) . ')'; } + /** + * @param $s + * @param $rc RecentChange + * @param $unpatrolled + * @return void + */ public function insertDiffHist( &$s, &$rc, $unpatrolled ) { # Diff link if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) { $diffLink = $this->message['diff']; - } else if( !self::userCan($rc,Revision::DELETED_TEXT) ) { + } elseif( !self::userCan($rc,Revision::DELETED_TEXT) ) { $diffLink = $this->message['diff']; } else { $query = array( @@ -264,31 +295,35 @@ class ChangesList { $query['rcid'] = $rc->mAttribs['rc_id']; }; - $diffLink = $this->skin->link( + $diffLink = Linker::linkKnown( $rc->getTitle(), $this->message['diff'], array( 'tabindex' => $rc->counter ), - $query, - array( 'known', 'noclasses' ) + $query ); } $s .= '(' . $diffLink . $this->message['pipe-separator']; # History link - $s .= $this->skin->link( + $s .= Linker::linkKnown( $rc->getTitle(), $this->message['hist'], array(), array( 'curid' => $rc->mAttribs['rc_cur_id'], 'action' => 'history' - ), - array( 'known', 'noclasses' ) + ) ); $s .= ') . . '; } + /** + * @param $s + * @param $rc RecentChange + * @param $unpatrolled + * @param $watched + * @return void + */ public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) { - global $wgContLang; # If it's a new article, there is no diff link, but if it hasn't been # patrolled yet, we need to give users a way to do so $params = array(); @@ -298,21 +333,19 @@ class ChangesList { } if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) { - $articlelink = $this->skin->link( + $articlelink = Linker::linkKnown( $rc->getTitle(), null, array(), - $params, - array( 'known', 'noclasses' ) + $params ); $articlelink = '' . $articlelink . ''; } else { - $articlelink = ' '. $this->skin->link( + $articlelink = ' '. Linker::linkKnown( $rc->getTitle(), null, array(), - $params, - array( 'known', 'noclasses' ) + $params ); } # Bolden pages watched by this user @@ -320,7 +353,7 @@ class ChangesList { $articlelink = "{$articlelink}"; } # RTL/LTR marker - $articlelink .= $wgContLang->getDirMark(); + $articlelink .= $this->getLang()->getDirMark(); wfRunHooks( 'ChangesListInsertArticleLink', array(&$this, &$articlelink, &$s, &$rc, $unpatrolled, $watched) ); @@ -328,41 +361,54 @@ class ChangesList { $s .= " $articlelink"; } + /** + * @param $s + * @param $rc RecentChange + * @return void + */ public function insertTimestamp( &$s, $rc ) { - global $wgLang; - $s .= $this->message['semicolon-separator'] . - $wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; + $s .= $this->message['semicolon-separator'] . + $this->getLang()->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . '; } - /** Insert links to user page, user talk page and eventually a blocking link */ + /** Insert links to user page, user talk page and eventually a blocking link + * + * @param $rc RecentChange + */ public function insertUserRelatedLinks( &$s, &$rc ) { if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { - $s .= ' ' . wfMsgHtml( 'rev-deleted-user' ) . ''; + $s .= ' ' . wfMsgHtml( 'rev-deleted-user' ) . ''; } else { - $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); - $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $s .= Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); } } - /** insert a formatted action */ + /** insert a formatted action + * + * @param $rc RecentChange + */ public function insertAction( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] == RC_LOG ) { if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) { $s .= ' ' . wfMsgHtml( 'rev-deleted-event' ) . ''; } else { $s .= ' '.LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], - $rc->getTitle(), $this->skin, LogPage::extractParams( $rc->mAttribs['rc_params'] ), true, true ); + $rc->getTitle(), $this->getSkin(), LogPage::extractParams( $rc->mAttribs['rc_params'] ), true, true ); } } } - /** insert a formatted comment */ + /** insert a formatted comment + * + * @param $rc RecentChange + */ public function insertComment( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) { $s .= ' ' . wfMsgHtml( 'rev-deleted-comment' ) . ''; } else { - $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); + $s .= Linker::commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); } } } @@ -380,12 +426,11 @@ class ChangesList { * Returns the string which indicates the number of watching users */ protected function numberofWatchingusers( $count ) { - global $wgLang; static $cache = array(); if( $count > 0 ) { if( !isset( $cache[$count] ) ) { $cache[$count] = wfMsgExt( 'number_of_watching_users_RCview', - array('parsemag', 'escape' ), $wgLang->formatNum( $count ) ); + array('parsemag', 'escape' ), $this->getLang()->formatNum( $count ) ); } return $cache[$count]; } else { @@ -425,15 +470,18 @@ class ChangesList { return '' . $link . ''; } } - - /** Inserts a rollback link */ + + /** Inserts a rollback link + * + * @param $s + * @param $rc RecentChange + */ public function insertRollback( &$s, &$rc ) { - global $wgUser; if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) { $page = $rc->getTitle(); /** Check for rollback and edit permissions, disallow special pages, and only * show a link on the top-most revision */ - if ($wgUser->isAllowed('rollback') && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] ) + if ( $this->getUser()->isAllowed('rollback') && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] ) { $rev = new Revision( array( 'id' => $rc->mAttribs['rc_this_oldid'], @@ -442,15 +490,21 @@ class ChangesList { 'deleted' => $rc->mAttribs['rc_deleted'] ) ); $rev->setTitle( $page ); - $s .= ' '.$this->skin->generateRollback( $rev ); + $s .= ' '.Linker::generateRollback( $rev, $this->getContext() ); } } } + /** + * @param $s + * @param $rc RecentChange + * @param $classes + * @return + */ public function insertTags( &$s, &$rc, &$classes ) { if ( empty($rc->mAttribs['ts_tags']) ) return; - + list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' ); $classes = array_merge( $classes, $newClasses ); $s .= ' ' . $tagSummary; @@ -468,12 +522,14 @@ class ChangesList { class OldChangesList extends ChangesList { /** * Format a line using the old system (aka without any javascript). + * + * @param $rc RecentChange */ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { - global $wgLang, $wgRCShowChangedSize, $wgUser; + global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); # Should patrol-related stuff be shown? - $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; + $unpatrolled = $this->getUser()->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; $dateheader = ''; // $s now contains only
  • ...
  • , for hooks' convenience. $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); @@ -499,7 +555,7 @@ class OldChangesList extends ChangesList { $this->insertLog( $s, $logtitle, $rc->mAttribs['rc_log_type'] ); // Log entries (old format) or log targets, and special pages } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { - list( $name, $subpage ) = SpecialPage::resolveAliasWithSubpage( $rc->mAttribs['rc_title'] ); + list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); if( $name == 'Log' ) { $this->insertLog( $s, $rc->getTitle(), $subpage ); } @@ -507,8 +563,15 @@ class OldChangesList extends ChangesList { } else { $this->insertDiffHist( $s, $rc, $unpatrolled ); # M, N, b and ! (minor, new, bot and unpatrolled) - $s .= $this->recentChangesFlags( $rc->mAttribs['rc_new'], $rc->mAttribs['rc_minor'], - $unpatrolled, '', $rc->mAttribs['rc_bot'] ); + $s .= $this->recentChangesFlags( + array( + 'newpage' => $rc->mAttribs['rc_new'], + 'minor' => $rc->mAttribs['rc_minor'], + 'unpatrolled' => $unpatrolled, + 'bot' => $rc->mAttribs['rc_bot'] + ), + '' + ); $this->insertArticleLink( $s, $rc, $unpatrolled, $watched ); } # Edit/log timestamp @@ -522,6 +585,8 @@ class OldChangesList extends ChangesList { } # User tool links $this->insertUserRelatedLinks( $s, $rc ); + # LTR/RTL direction mark + $s .= $this->getLang()->getDirMark(); # Log action text (if any) $this->insertAction( $s, $rc ); # Edit or log comment @@ -532,17 +597,17 @@ class OldChangesList extends ChangesList { $this->insertRollback( $s, $rc ); # For subclasses $this->insertExtra( $s, $rc, $classes ); - + # How many users watch this page if( $rc->numberofWatchingusers > 0 ) { - $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', - array( 'parsemag', 'escape' ), $wgLang->formatNum( $rc->numberofWatchingusers ) ); + $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', + array( 'parsemag', 'escape' ), $this->getLang()->formatNum( $rc->numberofWatchingusers ) ); } - + if( $this->watchlist ) { $classes[] = Sanitizer::escapeClass( 'watchlist-'.$rc->mAttribs['rc_namespace'].'-'.$rc->mAttribs['rc_title'] ); } - + wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) ); wfProfileOut( __METHOD__ ); @@ -560,34 +625,32 @@ class EnhancedChangesList extends ChangesList { * @return String */ public function beginRecentChangesList() { - global $wgOut; $this->rc_cache = array(); $this->rcMoveIndex = 0; $this->rcCacheIndex = 0; $this->lastdate = ''; $this->rclistOpen = false; - $wgOut->addModules( 'mediawiki.legacy.enhancedchanges' ); + $this->getOutput()->addModuleStyles( 'mediawiki.special.changeslist' ); return ''; } /** * Format a line for enhanced recentchange (aka with javascript and block of lines). + * + * @param $baseRC RecentChange + * @param $watched bool + * + * @return string */ public function recentChangesLine( &$baseRC, $watched = false ) { - global $wgLang, $wgUser; - wfProfileIn( __METHOD__ ); # Create a specialised object $rc = RCCacheEntry::newFromParent( $baseRC ); - # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something - // that explicitly initializes variables. - extract( $rc->mAttribs ); - $curIdEq = array( 'curid' => $rc_cur_id ); + $curIdEq = array( 'curid' => $rc->mAttribs['rc_cur_id'] ); # If it's a new day, add the headline and flush the cache - $date = $wgLang->date( $rc_timestamp, true ); + $date = $this->getLang()->date( $rc->mAttribs['rc_timestamp'], true ); $ret = ''; if( $date != $this->lastdate ) { # Process current cache @@ -598,48 +661,50 @@ class EnhancedChangesList extends ChangesList { } # Should patrol-related stuff be shown? - if( $wgUser->useRCPatrol() ) { - $rc->unpatrolled = !$rc_patrolled; + if( $this->getUser()->useRCPatrol() ) { + $rc->unpatrolled = !$rc->mAttribs['rc_patrolled']; } else { $rc->unpatrolled = false; } $showdifflinks = true; # Make article link + $type = $rc->mAttribs['rc_type']; + $logType = $rc->mAttribs['rc_log_type']; // Page moves - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; - $clink = wfMsg( $msg, $this->skin->linkKnown( $rc->getTitle(), null, + if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { + $msg = ( $type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir"; + $clink = wfMsg( $msg, Linker::linkKnown( $rc->getTitle(), null, array(), array( 'redirect' => 'no' ) ), - $this->skin->linkKnown( $rc->getMovedToTitle() ) ); + Linker::linkKnown( $rc->getMovedToTitle() ) ); // New unpatrolled pages - } else if( $rc->unpatrolled && $rc_type == RC_NEW ) { - $clink = $this->skin->linkKnown( $rc->getTitle(), null, array(), - array( 'rcid' => $rc_id ) ); + } elseif( $rc->unpatrolled && $type == RC_NEW ) { + $clink = Linker::linkKnown( $rc->getTitle(), null, array(), + array( 'rcid' => $rc->mAttribs['rc_id'] ) ); // Log entries - } else if( $rc_type == RC_LOG ) { - if( $rc_log_type ) { - $logtitle = SpecialPage::getTitleFor( 'Log', $rc_log_type ); - $clink = '(' . $this->skin->linkKnown( $logtitle, - LogPage::logName($rc_log_type) ) . ')'; + } elseif( $type == RC_LOG ) { + if( $logType ) { + $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); + $clink = '(' . Linker::linkKnown( $logtitle, + LogPage::logName( $logType ) ) . ')'; } else { - $clink = $this->skin->link( $rc->getTitle() ); + $clink = Linker::link( $rc->getTitle() ); } $watched = false; // Log entries (old format) and special pages - } elseif( $rc_namespace == NS_SPECIAL ) { - list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title ); + } elseif( $rc->mAttribs['rc_namespace'] == NS_SPECIAL ) { + list( $specialName, $logtype ) = SpecialPageFactory::resolveAlias( $rc->mAttribs['rc_title'] ); if ( $specialName == 'Log' ) { # Log updates, etc $logname = LogPage::logName( $logtype ); - $clink = '(' . $this->skin->linkKnown( $rc->getTitle(), $logname ) . ')'; + $clink = '(' . Linker::linkKnown( $rc->getTitle(), $logname ) . ')'; } else { wfDebug( "Unexpected special page in recentchanges\n" ); $clink = ''; } // Edits } else { - $clink = $this->skin->linkKnown( $rc->getTitle() ); + $clink = Linker::linkKnown( $rc->getTitle() ); } # Don't show unusable diff links @@ -647,7 +712,7 @@ class EnhancedChangesList extends ChangesList { $showdifflinks = false; } - $time = $wgLang->time( $rc_timestamp, true, true ); + $time = $this->getLang()->time( $rc->mAttribs['rc_timestamp'], true, true ); $rc->watched = $watched; $rc->link = $clink; $rc->timestamp = $time; @@ -655,20 +720,22 @@ class EnhancedChangesList extends ChangesList { # Make "cur" and "diff" links. Do not use link(), it is too slow if # called too many times (50% of CPU time on RecentChanges!). + $thisOldid = $rc->mAttribs['rc_this_oldid']; + $lastOldid = $rc->mAttribs['rc_last_oldid']; if( $rc->unpatrolled ) { - $rcIdQuery = array( 'rcid' => $rc_id ); + $rcIdQuery = array( 'rcid' => $rc->mAttribs['rc_id'] ); } else { $rcIdQuery = array(); } - $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $rc_this_oldid ); - $querydiff = $curIdEq + array( 'diff' => $rc_this_oldid, 'oldid' => - $rc_last_oldid ) + $rcIdQuery; + $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $thisOldid ); + $querydiff = $curIdEq + array( 'diff' => $thisOldid, 'oldid' => + $lastOldid ) + $rcIdQuery; if( !$showdifflinks ) { $curLink = $this->message['cur']; $diffLink = $this->message['diff']; - } else if( in_array( $rc_type, array(RC_NEW,RC_LOG,RC_MOVE,RC_MOVE_OVER_REDIRECT) ) ) { - if ( $rc_type != RC_NEW ) { + } elseif( in_array( $type, array( RC_NEW, RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) { + if ( $type != RC_NEW ) { $curLink = $this->message['cur']; } else { $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) ); @@ -683,21 +750,21 @@ class EnhancedChangesList extends ChangesList { } # Make "last" link - if( !$showdifflinks || !$rc_last_oldid ) { + if( !$showdifflinks || !$lastOldid ) { $lastLink = $this->message['last']; - } else if( $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + } elseif( in_array( $type, array( RC_LOG, RC_MOVE, RC_MOVE_OVER_REDIRECT ) ) ) { $lastLink = $this->message['last']; } else { - $lastLink = $this->skin->linkKnown( $rc->getTitle(), $this->message['last'], - array(), $curIdEq + array('diff' => $rc_this_oldid, 'oldid' => $rc_last_oldid) + $rcIdQuery ); + $lastLink = Linker::linkKnown( $rc->getTitle(), $this->message['last'], + array(), $curIdEq + array('diff' => $thisOldid, 'oldid' => $lastOldid) + $rcIdQuery ); } # Make user links - if( $this->isDeleted($rc,Revision::DELETED_USER) ) { - $rc->userlink = ' ' . wfMsgHtml( 'rev-deleted-user' ) . ''; + if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { + $rc->userlink = ' ' . wfMsgHtml( 'rev-deleted-user' ) . ''; } else { - $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); - $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); + $rc->userlink = Linker::userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); + $rc->usertalklink = Linker::userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); } $rc->lastlink = $lastLink; @@ -708,13 +775,13 @@ class EnhancedChangesList extends ChangesList { # Page moves go on their own line $title = $rc->getTitle(); $secureName = $title->getPrefixedDBkey(); - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { # Use an @ character to prevent collision with page names $this->rc_cache['@@' . ($this->rcMoveIndex++)] = array($rc); } else { # Logs are grouped by type - if( $rc_type == RC_LOG ){ - $secureName = SpecialPage::getTitleFor( 'Log', $rc_log_type )->getPrefixedDBkey(); + if( $type == RC_LOG ){ + $secureName = SpecialPage::getTitleFor( 'Log', $logType )->getPrefixedDBkey(); } if( !isset( $this->rc_cache[$secureName] ) ) { $this->rc_cache[$secureName] = array(); @@ -732,16 +799,16 @@ class EnhancedChangesList extends ChangesList { * Enhanced RC group */ protected function recentChangesBlockGroup( $block ) { - global $wgLang, $wgContLang, $wgRCShowChangedSize; + global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); # Add the namespace and title of the block as part of the class if ( $block[0]->mAttribs['rc_log_type'] ) { # Log entry - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); + $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $block[0]->mAttribs['rc_log_type'] . '-' . $block[0]->mAttribs['rc_title'] ); } else { - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); + $classes = 'mw-collapsible mw-collapsed mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); } $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); @@ -794,51 +861,56 @@ class EnhancedChangesList extends ChangesList { $users = array(); foreach( $userlinks as $userlink => $count) { $text = $userlink; - $text .= $wgContLang->getDirMark(); + $text .= $this->getLang()->getDirMark(); if( $count > 1 ) { - $text .= ' (' . $wgLang->formatNum( $count ) . '×)'; + $text .= ' (' . $this->getLang()->formatNum( $count ) . '×)'; } array_push( $users, $text ); } - $users = ' [' . + $users = ' [' . implode( $this->message['semicolon-separator'], $users ) . ']'; - # ID for JS visibility toggle - $jsid = $this->rcCacheIndex; - # onclick handler to toggle hidden/expanded - $toggleLink = "onclick='toggleVisibility($jsid); return false'"; # Title for tags $expandTitle = htmlspecialchars( wfMsg( 'rc-enhanced-expand' ) ); $closeTitle = htmlspecialchars( wfMsg( 'rc-enhanced-hide' ) ); - $tl = "" . $this->sideArrow() . ""; - $tl .= ""; - $r .= ''.$tl.' '; + $tl = "" + . "" + . "{$this->sideArrow()}" + . "" + . "{$this->downArrow()}" + . ""; + $r .= "$tl"; # Main line - $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); + $r .= '' . $this->recentChangesFlags( array( + 'newpage' => $isnew, + 'minor' => false, + 'unpatrolled' => $unpatrolled, + 'bot' => $bot , + ) ); # Timestamp - $r .= ' '.$block[0]->timestamp.' '; + $r .= ' '.$block[0]->timestamp.' '; # Article link if( $namehidden ) { $r .= ' ' . wfMsgHtml( 'rev-deleted-event' ) . ''; - } else if( $allLogs ) { + } elseif( $allLogs ) { $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); } else { $this->insertArticleLink( $r, $block[0], $block[0]->unpatrolled, $block[0]->watched ); } - $r .= $wgContLang->getDirMark(); + $r .= $this->getLang()->getDirMark(); $queryParams['curid'] = $curId; # Changes message $n = count($block); static $nchanges = array(); if ( !isset( $nchanges[$n] ) ) { - $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ); + $nchanges[$n] = wfMsgExt( 'nchanges', array( 'parsemag', 'escape' ), $this->getLang()->formatNum( $n ) ); } # Total change link $r .= ' '; @@ -846,14 +918,14 @@ class EnhancedChangesList extends ChangesList { $r .= '('; if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT ) ) { $r .= $nchanges[$n]; - } else if( $isnew ) { + } elseif( $isnew ) { $r .= $nchanges[$n]; } else { $params = $queryParams; $params['diff'] = $currentRevision; $params['oldid'] = $oldid; - - $r .= $this->skin->link( + + $r .= Linker::link( $block[0]->getTitle(), $nchanges[$n], array(), @@ -866,19 +938,18 @@ class EnhancedChangesList extends ChangesList { # History if( $allLogs ) { // don't show history link for logs - } else if( $namehidden || !$block[0]->getTitle()->exists() ) { + } elseif( $namehidden || !$block[0]->getTitle()->exists() ) { $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')'; } else { $params = $queryParams; $params['action'] = 'history'; $r .= $this->message['pipe-separator'] . - $this->skin->link( + Linker::linkKnown( $block[0]->getTitle(), $this->message['hist'], array(), - $params, - array( 'known', 'noclasses' ) + $params ) . ')'; } $r .= ' . . '; @@ -908,55 +979,51 @@ class EnhancedChangesList extends ChangesList { $r .= $users; $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers); - $r .= "\n"; - # Sub-entries - $r .= '
    '; - $r .= ''; foreach( $block as $rcObj ) { - # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something - // that explicitly initializes variables. # Classes to apply -- TODO implement $classes = array(); - extract( $rcObj->mAttribs ); + $type = $rcObj->mAttribs['rc_type']; #$r .= '\n"; } - $r .= "
    '.$this->spacerArrow(); - $r .= '
    '; - $r .= $this->spacerIndent() . $this->spacerIndent(); - $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); - $r .= ' '; + $r .= '
    '; + $r .= $this->recentChangesFlags( array( + 'newpage' => $rcObj->mAttribs['rc_new'], + 'minor' => $rcObj->mAttribs['rc_minor'], + 'unpatrolled' => $rcObj->unpatrolled, + 'bot' => $rcObj->mAttribs['rc_bot'], + ) ); + $r .= ' '; $params = $queryParams; - if( $rc_this_oldid != 0 ) { - $params['oldid'] = $rc_this_oldid; + if( $rcObj->mAttribs['rc_this_oldid'] != 0 ) { + $params['oldid'] = $rcObj->mAttribs['rc_this_oldid']; } # Log timestamp - if( $rc_type == RC_LOG ) { + if( $type == RC_LOG ) { $link = $rcObj->timestamp; # Revision link - } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { + } elseif( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { $link = ''.$rcObj->timestamp.' '; } else { - if ( $rcObj->unpatrolled && $rc_type == RC_NEW) { + if ( $rcObj->unpatrolled && $type == RC_NEW) { $params['rcid'] = $rcObj->mAttribs['rc_id']; } - $link = $this->skin->link( + $link = Linker::linkKnown( $rcObj->getTitle(), $rcObj->timestamp, array(), - $params, - array( 'known', 'noclasses' ) + $params ); if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) $link = ''.$link.' '; } $r .= $link . ''; - if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) { + if ( !$type == RC_LOG || $type == RC_NEW ) { $r .= ' ('; $r .= $rcObj->curlink; $r .= $this->message['pipe-separator']; @@ -966,9 +1033,10 @@ class EnhancedChangesList extends ChangesList { $r .= ' . . '; # Character diff - if( $wgRCShowChangedSize ) { - $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ; + if( $wgRCShowChangedSize && $rcObj->getCharacterDifference() ) { + $r .= $rcObj->getCharacterDifference() . ' . . ' ; } + # User links $r .= $rcObj->userlink; $r .= $rcObj->usertalklink; @@ -983,7 +1051,7 @@ class EnhancedChangesList extends ChangesList { $r .= "
    \n"; + $r .= "\n"; $this->rcCacheIndex++; @@ -1013,8 +1081,8 @@ class EnhancedChangesList extends ChangesList { * @return String: HTML tag */ protected function sideArrow() { - global $wgContLang; - $dir = $wgContLang->isRTL() ? 'l' : 'r'; + global $wgLang; + $dir = $wgLang->isRTL() ? 'l' : 'r'; return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) ); } @@ -1035,71 +1103,59 @@ class EnhancedChangesList extends ChangesList { return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space } - /** - * Add a set of spaces - * @return String: HTML tag - */ - protected function spacerIndent() { - return '     '; - } - /** * Enhanced RC ungrouped line. + * + * @param $rcObj RecentChange * @return String: a HTML formated line (generated using $r) */ protected function recentChangesBlockLine( $rcObj ) { global $wgRCShowChangedSize; wfProfileIn( __METHOD__ ); + $query['curid'] = $rcObj->mAttribs['rc_cur_id']; - # Extract fields from DB into the function scope (rc_xxxx variables) - // FIXME: Would be good to replace this extract() call with something - // that explicitly initializes variables. - // TODO implement - extract( $rcObj->mAttribs ); - $query['curid'] = $rc_cur_id; - - if( $rc_log_type ) { + $type = $rcObj->mAttribs['rc_type']; + $logType = $rcObj->mAttribs['rc_log_type']; + if( $logType ) { # Log entry - $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $rc_log_type . '-' . $rcObj->mAttribs['rc_title'] ); + $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType . '-' . $rcObj->mAttribs['rc_title'] ); } else { $classes = 'mw-enhanced-rc ' . Sanitizer::escapeClass( 'mw-changeslist-ns' . $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] ); } $r = Html::openElement( 'table', array( 'class' => $classes ) ) . Html::openElement( 'tr' ); - $r .= '' . $this->spacerArrow() . ' '; + $r .= '' . $this->spacerArrow(); # Flag and Timestamp - if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + if( $type == RC_MOVE || $type == RC_MOVE_OVER_REDIRECT ) { $r .= '    '; // 4 flags -> 4 spaces } else { - $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); + $r .= $this->recentChangesFlags( array( + 'newpage' => $type == RC_NEW, + 'minor' => $rcObj->mAttribs['rc_minor'], + 'unpatrolled' => $rcObj->unpatrolled, + 'bot' => $rcObj->mAttribs['rc_bot'], + ) ); } - $r .= ' '.$rcObj->timestamp.' '; + $r .= ' '.$rcObj->timestamp.' '; # Article or log link - if( $rc_log_type ) { - $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL ); - $logname = LogPage::logName( $rc_log_type ); - $r .= '(' . $this->skin->link( - $logtitle, - $logname, - array(), - array(), - array( 'known', 'noclasses' ) - ) . ')'; + if( $logType ) { + $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); + $logname = LogPage::logName( $logType ); + $r .= '(' . Linker::linkKnown( $logtitle, htmlspecialchars( $logname ) ) . ')'; } else { $this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched ); } # Diff and hist links - if ( $rc_type != RC_LOG ) { + if ( $type != RC_LOG ) { $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator']; $query['action'] = 'history'; - $r .= $this->skin->link( + $r .= Linker::linkKnown( $rcObj->getTitle(), $this->message['hist'], array(), - $query, - array( 'known', 'noclasses' ) + $query ) . ')'; } $r .= ' . . '; @@ -1110,12 +1166,12 @@ class EnhancedChangesList extends ChangesList { # User/talk $r .= ' '.$rcObj->userlink . $rcObj->usertalklink; # Log action (if any) - if( $rc_log_type ) { + if( $logType ) { if( $this->isDeleted($rcObj,LogPage::DELETED_ACTION) ) { $r .= ' ' . wfMsgHtml('rev-deleted-event') . ''; } else { - $r .= ' ' . LogPage::actionText( $rc_log_type, $rc_log_action, $rcObj->getTitle(), - $this->skin, LogPage::extractParams($rc_params), true, true ); + $r .= ' ' . LogPage::actionText( $logType, $rcObj->mAttribs['rc_log_action'], $rcObj->getTitle(), + $this->getSkin(), LogPage::extractParams( $rcObj->mAttribs['rc_params'] ), true, true ); } } $this->insertComment( $r, $rcObj ); @@ -1136,6 +1192,8 @@ class EnhancedChangesList extends ChangesList { /** * If enhanced RC is in use, this function takes the previously cached * RC lines, arranges them, and outputs the HTML + * + * @return string */ protected function recentChangesBlock() { if( count ( $this->rc_cache ) == 0 ) { @@ -1159,7 +1217,7 @@ class EnhancedChangesList extends ChangesList { } /** - * Returns text for the end of RC + * Returns text for the end of RC * If enhanced RC is in use, returns pretty much all the text */ public function endRecentChangesList() { diff --git a/includes/Collation.php b/includes/Collation.php index f00b568f..0c510b78 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -3,6 +3,9 @@ abstract class Collation { static $instance; + /** + * @return Collation + */ static function singleton() { if ( !self::$instance ) { global $wgCategoryCollation; @@ -11,13 +14,30 @@ abstract class Collation { return self::$instance; } + /** + * @throws MWException + * @param $collationName string + * @return Collation + */ static function factory( $collationName ) { switch( $collationName ) { case 'uppercase': return new UppercaseCollation; + case 'identity': + return new IdentityCollation; case 'uca-default': return new IcuCollation( 'root' ); default: + # Provide a mechanism for extensions to hook in. + + $collationObject = null; + wfRunHooks( 'Collation::factory', array( $collationName, &$collationObject ) ); + + if ( $collationObject instanceof Collation ) { + return $collationObject; + } + + // If all else fails... throw new MWException( __METHOD__.": unknown collation type \"$collationName\"" ); } } @@ -81,6 +101,30 @@ class UppercaseCollation extends Collation { } } +/** + * Collation class that's essentially a no-op. + * + * Does sorting based on binary value of the string. + * Like how things were pre 1.17. + */ +class IdentityCollation extends Collation { + + function getSortKey( $string ) { + return $string; + } + + function getFirstLetter( $string ) { + global $wgContLang; + // Copied from UppercaseCollation. + // I'm kind of unclear on when this could happen... + if ( $string[0] == "\0" ) { + $string = substr( $string, 1 ); + } + return $wgContLang->firstChar( $string ); + } +} + + class IcuCollation extends Collation { var $primaryCollator, $mainCollator, $locale; var $firstLetterData; @@ -259,13 +303,13 @@ class IcuCollation extends Collation { * Do a binary search, and return the index of the largest item that sorts * less than or equal to the target value. * - * @param $valueCallback A function to call to get the value with + * @param $valueCallback array A function to call to get the value with * a given array index. - * @param $valueCount The number of items accessible via $valueCallback, + * @param $valueCount int The number of items accessible via $valueCallback, * indexed from 0 to $valueCount - 1 - * @param $comparisonCallback A callback to compare two values, returning + * @param $comparisonCallback array A callback to compare two values, returning * -1, 0 or 1 in the style of strcmp(). - * @param $target The target value to find. + * @param $target string The target value to find. * * @return The item index of the lower bound, or false if the target value * sorts before all items. diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php index b08b77df..42a7173d 100644 --- a/includes/ConfEditor.php +++ b/includes/ConfEditor.php @@ -1,6 +1,6 @@ 'delete', 'path' => '$foo/bar/baz' ) * is equivalent to the runtime PHP code: * unset( $foo['bar']['baz'] ); * * set - * Sets the value of an array element. If the element doesn't exist, it - * is appended to the array. If it does exist, the value is set, with + * Sets the value of an array element. If the element doesn't exist, it + * is appended to the array. If it does exist, the value is set, with * comments and indenting preserved. * * append * Appends a new element to the end of the array. Adds a trailing comma. * e.g. - * array( 'type' => 'append', 'path', '$foo/bar', + * array( 'type' => 'append', 'path', '$foo/bar', * 'key' => 'baz', 'value' => "'x'" ) * is like the PHP code: * $foo['bar']['baz'] = 'x'; @@ -187,7 +191,7 @@ class ConfEditor { list( $indent, ) = $this->getIndent( $start ); $textToInsert = "$indent$value,"; } else { - list( $indent, $arrowIndent ) = + list( $indent, $arrowIndent ) = $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); $textToInsert = "$indent$key$arrowIndent=> $value,"; } @@ -210,7 +214,7 @@ class ConfEditor { list( $indent, ) = $this->getIndent( $start ); $textToInsert = "$indent$value,"; } else { - list( $indent, $arrowIndent ) = + list( $indent, $arrowIndent ) = $this->getIndent( $start, $key, $info['arrowByte'] ); $textToInsert = "$indent$key$arrowIndent=> $value,"; } @@ -239,13 +243,13 @@ class ConfEditor { try { $this->parse(); } catch ( ConfEditorParseError $e ) { - throw new MWException( + throw new MWException( "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " . $e->getMessage() ); } return $out; } - + /** * Get the variables defined in the text * @return array( varname => value ) @@ -266,7 +270,7 @@ class ConfEditor { strlen( $trimmedPath ) - strlen( $name ) ); if( substr( $parentPath, -1 ) == '/' ) $parentPath = substr( $parentPath, 0, -1 ); - + $value = substr( $this->text, $data['valueStartByte'], $data['valueEndByte'] - $data['valueStartByte'] ); @@ -275,7 +279,7 @@ class ConfEditor { } return $vars; } - + /** * Set a value in an array, unless it's set already. For instance, * setVar( $arr, 'foo/bar', 'baz', 3 ); will set @@ -298,7 +302,7 @@ class ConfEditor { if ( !isset( $target[$key] ) ) $target[$key] = $value; } - + /** * Parse a scalar value in PHP * @return mixed Parsed value @@ -306,14 +310,14 @@ class ConfEditor { function parseScalar( $str ) { if ( $str !== '' && $str[0] == '\'' ) // Single-quoted string - // @todo Fixme: trim() call is due to mystery bug where whitespace gets + // @todo FIXME: trim() call is due to mystery bug where whitespace gets // appended to the token; without it we ended up reading in the // extra quote on the end! return strtr( substr( trim( $str ), 1, -1 ), array( '\\\'' => '\'', '\\\\' => '\\' ) ); - if ( $str !== '' && @$str[0] == '"' ) + if ( $str !== '' && $str[0] == '"' ) // Double-quoted string - // @todo Fixme: trim() call is due to mystery bug where whitespace gets + // @todo FIXME: trim() call is due to mystery bug where whitespace gets // appended to the token; without it we ended up reading in the // extra quote on the end! return stripcslashes( substr( trim( $str ), 1, -1 ) ); @@ -364,8 +368,8 @@ class ConfEditor { } /** - * Finds the source byte region which you would want to delete, if $pathName - * was to be deleted. Includes the leading spaces and tabs, the trailing line + * Finds the source byte region which you would want to delete, if $pathName + * was to be deleted. Includes the leading spaces and tabs, the trailing line * break, and any comments in between. */ function findDeletionRegion( $pathName ) { @@ -419,9 +423,9 @@ class ConfEditor { } /** - * Find the byte region in the source corresponding to the value part. - * This includes the quotes, but does not include the trailing comma - * or semicolon. + * Find the byte region in the source corresponding to the value part. + * This includes the quotes, but does not include the trailing comma + * or semicolon. * * The end position is the past-the-end (end + 1) value as per convention. */ @@ -472,7 +476,7 @@ class ConfEditor { return $extraPath; } - /* + /** * Find the path name of first element in the array. * If the array is empty, this will return the \@extra interstitial element. * If the specified path is not found or is not an array, it will return false. @@ -519,7 +523,7 @@ class ConfEditor { } /** - * Run the parser on the text. Throws an exception if the string does not + * Run the parser on the text. Throws an exception if the string does not * match our defined subset of PHP syntax. */ public function parse() { @@ -706,7 +710,7 @@ class ConfEditor { } /** - * Set the parse position. Do not call this except from firstToken() and + * Set the parse position. Do not call this except from firstToken() and * nextToken(), there is more to update than just the position. */ protected function setPos( $pos ) { @@ -800,7 +804,7 @@ class ConfEditor { if ( $this->currentToken && $this->currentToken->type == $type ) { return $this->nextToken(); } else { - $this->error( "expected " . $this->getTypeName( $type ) . + $this->error( "expected " . $this->getTypeName( $type ) . ", got " . $this->getTypeName( $this->currentToken->type ) ); } } @@ -875,7 +879,7 @@ class ConfEditor { } /** - * Go to the next path on the same level. This ends the current path and + * Go to the next path on the same level. This ends the current path and * starts a new one. If $path is \@next, the new path is set to the next * numeric array element. */ @@ -889,7 +893,7 @@ class ConfEditor { } else { $this->pathStack[$i]['name'] = $path; } - $this->pathStack[$i] = + $this->pathStack[$i] = array( 'startByte' => $this->byteNum, 'startToken' => $this->pos, @@ -955,8 +959,8 @@ class ConfEditor { } /** - * Looks ahead to see if the given type is the next token type, starting - * from the current position plus the given offset. Skips any intervening + * Looks ahead to see if the given type is the next token type, starting + * from the current position plus the given offset. Skips any intervening * whitespace. */ function isAhead( $type, $offset = 0 ) { @@ -992,8 +996,8 @@ class ConfEditor { $out = ''; foreach ( $this->tokens as $token ) { $obj = $this->newTokenObj( $token ); - $out .= sprintf( "%-28s %s\n", - $this->getTypeName( $obj->type ), + $out .= sprintf( "%-28s %s\n", + $this->getTypeName( $obj->type ), addcslashes( $obj->text, "\0..\37" ) ); } echo "
    " . htmlspecialchars( $out ) . "
    "; @@ -1008,7 +1012,7 @@ class ConfEditorParseError extends MWException { function __construct( $editor, $msg ) { $this->lineNum = $editor->lineNum; $this->colNum = $editor->colNum; - parent::__construct( "Parse error on line {$editor->lineNum} " . + parent::__construct( "Parse error on line {$editor->lineNum} " . "col {$editor->colNum}: $msg" ); } @@ -1028,7 +1032,7 @@ class ConfEditorParseError extends MWException { */ class ConfEditorToken { var $type, $text; - + static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); diff --git a/includes/Cookie.php b/includes/Cookie.php new file mode 100644 index 00000000..95a4599f --- /dev/null +++ b/includes/Cookie.php @@ -0,0 +1,245 @@ +name = $name; + $this->set( $value, $attr ); + } + + /** + * Sets a cookie. Used before a request to set up any individual + * cookies. Used internally after a request to parse the + * Set-Cookie headers. + * + * @param $value String: the value of the cookie + * @param $attr Array: possible key/values: + * expires A date string + * path The path this cookie is used on + * domain Domain this cookie is used on + */ + public function set( $value, $attr ) { + $this->value = $value; + + if ( isset( $attr['expires'] ) ) { + $this->isSessionKey = false; + $this->expires = strtotime( $attr['expires'] ); + } + + if ( isset( $attr['path'] ) ) { + $this->path = $attr['path']; + } else { + $this->path = '/'; + } + + if ( isset( $attr['domain'] ) ) { + if ( self::validateCookieDomain( $attr['domain'] ) ) { + $this->domain = $attr['domain']; + } + } else { + throw new MWException( 'You must specify a domain.' ); + } + } + + /** + * Return the true if the cookie is valid is valid. Otherwise, + * false. The uses a method similar to IE cookie security + * described here: + * http://kuza55.blogspot.com/2008/02/understanding-cookie-security.html + * A better method might be to use a blacklist like + * http://publicsuffix.org/ + * + * @fixme fails to detect 3-letter top-level domains + * @fixme fails to detect 2-letter top-level domains for single-domain use (probably not a big problem in practice, but there are test cases) + * + * @param $domain String: the domain to validate + * @param $originDomain String: (optional) the domain the cookie originates from + * @return Boolean + */ + public static function validateCookieDomain( $domain, $originDomain = null ) { + // Don't allow a trailing dot + if ( substr( $domain, -1 ) == '.' ) { + return false; + } + + $dc = explode( ".", $domain ); + + // Only allow full, valid IP addresses + if ( preg_match( '/^[0-9.]+$/', $domain ) ) { + if ( count( $dc ) != 4 ) { + return false; + } + + if ( ip2long( $domain ) === false ) { + return false; + } + + if ( $originDomain == null || $originDomain == $domain ) { + return true; + } + + } + + // Don't allow cookies for "co.uk" or "gov.uk", etc, but allow "supermarket.uk" + if ( strrpos( $domain, "." ) - strlen( $domain ) == -3 ) { + if ( ( count( $dc ) == 2 && strlen( $dc[0] ) <= 2 ) + || ( count( $dc ) == 3 && strlen( $dc[0] ) == "" && strlen( $dc[1] ) <= 2 ) ) { + return false; + } + if ( ( count( $dc ) == 2 || ( count( $dc ) == 3 && $dc[0] == '' ) ) + && preg_match( '/(com|net|org|gov|edu)\...$/', $domain ) ) { + return false; + } + } + + if ( $originDomain != null ) { + if ( substr( $domain, 0, 1 ) != '.' && $domain != $originDomain ) { + return false; + } + + if ( substr( $domain, 0, 1 ) == '.' + && substr_compare( $originDomain, $domain, -strlen( $domain ), + strlen( $domain ), true ) != 0 ) { + return false; + } + } + + return true; + } + + /** + * Serialize the cookie jar into a format useful for HTTP Request headers. + * + * @param $path String: the path that will be used. Required. + * @param $domain String: the domain that will be used. Required. + * @return String + */ + public function serializeToHttpRequest( $path, $domain ) { + $ret = ''; + + if ( $this->canServeDomain( $domain ) + && $this->canServePath( $path ) + && $this->isUnExpired() ) { + $ret = $this->name . '=' . $this->value; + } + + return $ret; + } + + protected function canServeDomain( $domain ) { + if ( $domain == $this->domain + || ( strlen( $domain ) > strlen( $this->domain ) + && substr( $this->domain, 0, 1 ) == '.' + && substr_compare( $domain, $this->domain, -strlen( $this->domain ), + strlen( $this->domain ), true ) == 0 ) ) { + return true; + } + + return false; + } + + protected function canServePath( $path ) { + if ( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ) { + return true; + } + + return false; + } + + protected function isUnExpired() { + if ( $this->isSessionKey || $this->expires > time() ) { + return true; + } + + return false; + } +} + +class CookieJar { + private $cookie = array(); + + /** + * Set a cookie in the cookie jar. Make sure only one cookie per-name exists. + * @see Cookie::set() + */ + public function setCookie ( $name, $value, $attr ) { + /* cookies: case insensitive, so this should work. + * We'll still send the cookies back in the same case we got them, though. + */ + $index = strtoupper( $name ); + + if ( isset( $this->cookie[$index] ) ) { + $this->cookie[$index]->set( $value, $attr ); + } else { + $this->cookie[$index] = new Cookie( $name, $value, $attr ); + } + } + + /** + * @see Cookie::serializeToHttpRequest + */ + public function serializeToHttpRequest( $path, $domain ) { + $cookies = array(); + + foreach ( $this->cookie as $c ) { + $serialized = $c->serializeToHttpRequest( $path, $domain ); + + if ( $serialized ) { + $cookies[] = $serialized; + } + } + + return implode( '; ', $cookies ); + } + + /** + * Parse the content of an Set-Cookie HTTP Response header. + * + * @param $cookie String + * @param $domain String: cookie's domain + */ + public function parseCookieResponseHeader ( $cookie, $domain ) { + $len = strlen( 'Set-Cookie:' ); + + if ( substr_compare( 'Set-Cookie:', $cookie, 0, $len, true ) === 0 ) { + $cookie = substr( $cookie, $len ); + } + + $bit = array_map( 'trim', explode( ';', $cookie ) ); + + if ( count( $bit ) >= 1 ) { + list( $name, $value ) = explode( '=', array_shift( $bit ), 2 ); + $attr = array(); + + foreach ( $bit as $piece ) { + $parts = explode( '=', $piece ); + if ( count( $parts ) > 1 ) { + $attr[strtolower( $parts[0] )] = $parts[1]; + } else { + $attr[strtolower( $parts[0] )] = true; + } + } + + if ( !isset( $attr['domain'] ) ) { + $attr['domain'] = $domain; + } elseif ( !Cookie::validateCookieDomain( $attr['domain'], $domain ) ) { + return null; + } + + $this->setCookie( $name, $value, $attr ); + } + } +} diff --git a/includes/Credits.php b/includes/Credits.php deleted file mode 100644 index e4c8be54..00000000 --- a/includes/Credits.php +++ /dev/null @@ -1,238 +0,0 @@ -. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - * - * @file - * @author - */ - -class Credits { - /** - * This is largely cadged from PageHistory::history - * @param $article Article object - */ - public static function showPage( Article $article ) { - global $wgOut; - - wfProfileIn( __METHOD__ ); - - $wgOut->setPageTitle( $article->mTitle->getPrefixedText() ); - $wgOut->setSubtitle( wfMsg( 'creditspage' ) ); - $wgOut->setArticleFlag( false ); - $wgOut->setArticleRelated( true ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - - if ( $article->mTitle->getArticleID() == 0 ) { - $s = wfMsg( 'nocredits' ); - } else { - $s = self::getCredits( $article, -1 ); - } - - $wgOut->addHTML( $s ); - - wfProfileOut( __METHOD__ ); - } - - /** - * Get a list of contributors of $article - * @param $article Article object - * @param $cnt Int: maximum list of contributors to show - * @param $showIfMax Bool: whether to contributors if there more than $cnt - * @return String: html - */ - public static function getCredits( Article $article, $cnt, $showIfMax = true ) { - wfProfileIn( __METHOD__ ); - $s = ''; - - if ( isset( $cnt ) && $cnt != 0 ) { - $s = self::getAuthor( $article ); - if ( $cnt > 1 || $cnt < 0 ) { - $s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax ); - } - } - - wfProfileOut( __METHOD__ ); - return $s; - } - - /** - * Get the last author with the last modification time - * @param $article Article object - */ - protected static function getAuthor( Article $article ) { - global $wgLang; - - $user = User::newFromId( $article->getUser() ); - - $timestamp = $article->getTimestamp(); - if ( $timestamp ) { - $d = $wgLang->date( $article->getTimestamp(), true ); - $t = $wgLang->time( $article->getTimestamp(), true ); - } else { - $d = ''; - $t = ''; - } - return wfMsgExt( 'lastmodifiedatby', 'parsemag', $d, $t, self::userLink( $user ), $user->getName() ); - } - - /** - * Get a list of contributors of $article - * @param $article Article object - * @param $cnt Int: maximum list of contributors to show - * @param $showIfMax Bool: whether to contributors if there more than $cnt - * @return String: html - */ - protected static function getContributors( Article $article, $cnt, $showIfMax ) { - global $wgLang, $wgHiddenPrefs; - - $contributors = $article->getContributors(); - - $others_link = false; - - # Hmm... too many to fit! - if ( $cnt > 0 && $contributors->count() > $cnt ) { - $others_link = self::othersLink( $article ); - if ( !$showIfMax ) - return wfMsgExt( 'othercontribs', 'parsemag', $others_link, $contributors->count() ); - } - - $real_names = array(); - $user_names = array(); - $anon_ips = array(); - - # Sift for real versus user names - foreach ( $contributors as $user ) { - $cnt--; - if ( $user->isLoggedIn() ) { - $link = self::link( $user ); - if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { - $real_names[] = $link; - } else { - $user_names[] = $link; - } - } else { - $anon_ips[] = self::link( $user ); - } - - if ( $cnt == 0 ) { - break; - } - } - - if ( count( $real_names ) ) { - $real = $wgLang->listToText( $real_names ); - } else { - $real = false; - } - - # "ThisSite user(s) A, B and C" - if ( count( $user_names ) ) { - $user = wfMsgExt( - 'siteusers', - 'parsemag', - $wgLang->listToText( $user_names ), count( $user_names ) - ); - } else { - $user = false; - } - - if ( count( $anon_ips ) ) { - $anon = wfMsgExt( - 'anonusers', - 'parsemag', - $wgLang->listToText( $anon_ips ), count( $anon_ips ) - ); - } else { - $anon = false; - } - - # This is the big list, all mooshed together. We sift for blank strings - $fulllist = array(); - foreach ( array( $real, $user, $anon, $others_link ) as $s ) { - if ( $s ) { - array_push( $fulllist, $s ); - } - } - - # Make the list into text... - $creds = $wgLang->listToText( $fulllist ); - - # "Based on work by ..." - return strlen( $creds ) - ? wfMsgExt( 'othercontribs', 'parsemag', $creds, count( $fulllist ) ) - : ''; - } - - /** - * Get a link to $user's user page - * @param $user User object - * @return String: html - */ - protected static function link( User $user ) { - global $wgUser, $wgHiddenPrefs; - if ( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() ) { - $real = $user->getRealName(); - } else { - $real = false; - } - - $skin = $wgUser->getSkin(); - $page = $user->isAnon() ? - SpecialPage::getTitleFor( 'Contributions', $user->getName() ) : - $user->getUserPage(); - - return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) ); - } - - /** - * Get a link to $user's user page - * @param $user User object - * @return String: html - */ - protected static function userLink( User $user ) { - $link = self::link( $user ); - if ( $user->isAnon() ) { - return wfMsgExt( 'anonuser', array( 'parseinline', 'replaceafter' ), $link ); - } else { - global $wgHiddenPrefs; - if ( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() ) { - return $link; - } else { - return wfMsgExt( 'siteuser', 'parsemag', $link, $user->getName() ); - } - } - } - - /** - * Get a link to action=credits of $article page - * @param $article Article object - * @return String: html - */ - protected static function othersLink( Article $article ) { - global $wgUser; - $skin = $wgUser->getSkin(); - return $skin->link( - $article->getTitle(), - wfMsgHtml( 'others' ), - array(), - array( 'action' => 'credits' ), - array( 'known' ) - ); - } -} diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 0395633d..4248add7 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -26,58 +26,43 @@ if( !defined( 'MEDIAWIKI' ) ) { die( 1 ); } -# Create a site configuration object. Not used for much in a default install -if ( !defined( 'MW_PHP4' ) ) { - require_once( "$IP/includes/SiteConfiguration.php" ); - $wgConf = new SiteConfiguration; -} +# Create a site configuration object. Not used for much in a default install. +# Note: this (and other things) will break if the autoloader is not enabled. +# Please include includes/AutoLoader.php before including this file. +$wgConf = new SiteConfiguration; /** @endcond */ /** MediaWiki version number */ -$wgVersion = '1.17.1'; +$wgVersion = '1.18.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; /** - * URL of the server. It will be automatically built including https mode. + * URL of the server. * * Example: * - * $wgServer = http://example.com + * $wgServer = 'http://example.com'; * * * This is usually detected correctly by MediaWiki. If MediaWiki detects the * wrong server, it will redirect incorrectly after you save a page. In that * case, set this variable to fix it. + * + * If you want to use protocol-relative URLs on your wiki, set this to a + * protocol-relative URL like '//example.com' and set $wgCanonicalServer + * to a fully qualified URL. */ -$wgServer = ''; - -/** @cond file_level_code */ -if( isset( $_SERVER['SERVER_NAME'] ) ) { - $serverName = $_SERVER['SERVER_NAME']; -} elseif( isset( $_SERVER['HOSTNAME'] ) ) { - $serverName = $_SERVER['HOSTNAME']; -} elseif( isset( $_SERVER['HTTP_HOST'] ) ) { - $serverName = $_SERVER['HTTP_HOST']; -} elseif( isset( $_SERVER['SERVER_ADDR'] ) ) { - $serverName = $_SERVER['SERVER_ADDR']; -} else { - $serverName = 'localhost'; -} - -$wgProto = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http'; +$wgServer = WebRequest::detectServer(); -$wgServer = $wgProto.'://' . $serverName; -# If the port is a non-standard one, add it to the URL -if( isset( $_SERVER['SERVER_PORT'] ) - && !strpos( $serverName, ':' ) - && ( ( $wgProto == 'http' && $_SERVER['SERVER_PORT'] != 80 ) - || ( $wgProto == 'https' && $_SERVER['SERVER_PORT'] != 443 ) ) ) { - - $wgServer .= ":" . $_SERVER['SERVER_PORT']; -} -/** @endcond */ +/** + * Canonical URL of the server, to use in IRC feeds and notification e-mails. + * Must be fully qualified, even if $wgServer is protocol-relative. + * + * Defaults to $wgServer, expanded to a fully qualified http:// URL if needed. + */ +$wgCanonicalServer = false; /************************************************************************//** * @name Script path settings @@ -147,6 +132,7 @@ $wgRedirectScript = false; ///< defaults to */ $wgLoadScript = false; + /**@}*/ /************************************************************************//** @@ -183,6 +169,7 @@ $wgLocalStylePath = false; /** * The URL path of the extensions directory. * Defaults to "{$wgScriptPath}/extensions". + * @since 1.16 */ $wgExtensionAssetsPath = false; @@ -227,23 +214,6 @@ $wgFavicon = '/favicon.ico'; */ $wgAppleTouchIcon = false; -/** - * The URL path of the math directory. Defaults to "{$wgUploadPath}/math". - * - * See http://www.mediawiki.org/wiki/Manual:Enable_TeX for details about how to - * set up mathematical formula display. - */ -$wgMathPath = false; - -/** - * The filesystem path of the math directory. - * Defaults to "{$wgUploadDirectory}/math". - * - * See http://www.mediawiki.org/wiki/Manual:Enable_TeX for details about how to - * set up mathematical formula display. - */ -$wgMathDirectory = false; - /** * The local filesystem path to a temporary directory. This is not required to * be web accessible. @@ -295,7 +265,7 @@ $wgAllowImageMoving = true; $wgIllegalFileChars = ":"; /** - * @deprecated use $wgDeletedDirectory + * @deprecated since 1.17 use $wgDeletedDirectory */ $wgFileStore = array(); @@ -327,7 +297,7 @@ $wgImgAuthPublicTest = true; * - class The class name for the repository. May come from the core or an extension. * The core repository classes are LocalRepo, ForeignDBRepo, FSRepo. * - * - name A unique name for the repository. + * - name A unique name for the repository (but $wgLocalFileRepo should be 'local'). * * For most core repos: * - url Base public URL @@ -373,8 +343,12 @@ $wgImgAuthPublicTest = true; * - apibase Use for the foreign API's URL * - apiThumbCacheExpiry How long to locally cache thumbs for * - * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: - * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc. + * If you leave $wgLocalFileRepo set to false, Setup will fill in appropriate values. + * Otherwise, set $wgLocalFileRepo to a repository structure as described above. + * If you set $wgUseInstantCommons to true, it will add an entry for Commons. + * If you set $wgForeignFileRepos to an array of repostory structures, those will + * be searched after the local file repo. + * Otherwise, you will only have access to local media files. */ $wgLocalFileRepo = false; @@ -393,7 +367,7 @@ $wgUseInstantCommons = false; * Requires PHP's EXIF extension: http://www.php.net/manual/en/ref.exif.php * * NOTE FOR WINDOWS USERS: - * To enable EXIF functions, add the folloing lines to the + * To enable EXIF functions, add the following lines to the * "Windows extensions" section of php.ini: * * extension=extensions/php_mbstring.dll @@ -401,6 +375,13 @@ $wgUseInstantCommons = false; */ $wgShowEXIF = function_exists( 'exif_read_data' ); +/** + * If to automatically update the img_metadata field + * if the metadata field is outdated but compatible with the current version. + * Defaults to false. + */ +$wgUpdateCompatibleMetadata = false; + /** * If you operate multiple wikis, you can define a shared upload path here. * Uploads to this wiki will NOT be put there - they will be put into @@ -434,12 +415,24 @@ $wgCacheSharedUploads = true; $wgAllowCopyUploads = false; /** * Allow asynchronous copy uploads. - * This feature is experimental. + * This feature is experimental and broken as of r81612. */ $wgAllowAsyncCopyUploads = false; /** - * Max size for uploads, in bytes. Applies to all uploads. + * Max size for uploads, in bytes. If not set to an array, applies to all + * uploads. If set to an array, per upload type maximums can be set, using the + * file and url keys. If the * key is set this value will be used as maximum + * for non-specified types. + * + * For example: + * $wgMaxUploadSize = array( + * '*' => 250 * 1024, + * 'url' => 500 * 1024, + * ); + * Sets the maximum for all uploads to 250 kB except for upload-by-url, which + * will have a maximum of 500 kB. + * */ $wgMaxUploadSize = 1024*1024*100; # 100MB @@ -536,21 +529,15 @@ $wgMimeTypeBlacklist = array( 'text/scriptlet', 'application/x-msdownload', # Windows metafile, client-side vulnerability on some systems 'application/x-msmetafile', - # A ZIP file may be a valid Java archive containing an applet which exploits the - # same-origin policy to steal cookies - 'application/zip', - - # MS Office OpenXML and other Open Package Conventions files are zip files - # and thus blacklisted just as other zip files. If you remove these entries - # from the blacklist in your local configuration, a malicious file upload - # will be able to compromise the wiki's user accounts, and the user - # accounts of any other website in the same cookie domain. - 'application/x-opc+zip', - 'application/msword', - 'application/vnd.ms-powerpoint', - 'application/vnd.msexcel', ); +/** + * Allow Java archive uploads. + * This is not recommended for public wikis since a maliciously-constructed + * applet running on the same domain as the wiki can steal the user's cookies. + */ +$wgAllowJavaUploads = false; + /** * This is a flag to determine whether or not to check file extensions on upload. * @@ -593,7 +580,7 @@ $wgTrustedMediaFormats = array( * Each entry in the array maps a MIME type to a class name */ $wgMediaHandlers = array( - 'image/jpeg' => 'BitmapHandler', + 'image/jpeg' => 'JpegHandler', 'image/png' => 'PNGHandler', 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', @@ -644,12 +631,19 @@ $wgImageMagickTempDir = false; */ $wgCustomConvertCommand = false; +/** + * Some tests and extensions use exiv2 to manipulate the EXIF metadata in some image formats. + */ +$wgExiv2Command = '/usr/bin/exiv2'; + /** * Scalable Vector Graphics (SVG) may be uploaded as images. * Since SVG support is not yet standard in browsers, it is * necessary to rasterize SVGs to PNG as a fallback format. * * An external program is required to perform this conversion. + * If set to an array, the first item is a PHP callable and any further items + * are passed as parameters after $srcPath, $dstPath, $width, $height */ $wgSVGConverters = array( 'ImageMagick' => '$path/convert -background white -thumbnail $widthx$height\! $input PNG:$output', @@ -658,6 +652,7 @@ $wgSVGConverters = array( 'batik' => 'java -Djava.awt.headless=true -jar $path/batik-rasterizer.jar -w $width -d $output $input', 'rsvg' => '$path/rsvg -w$width -h$height $input $output', 'imgserv' => '$path/imgserv-wrapper -i svg -o png -w$width $input $output', + 'ImagickExt' => array( 'SvgHandler::rasterizeImagickExt' ), ); /** Pick a converter defined in $wgSVGConverters */ $wgSVGConverter = 'ImageMagick'; @@ -667,7 +662,7 @@ $wgSVGConverterPath = ''; $wgSVGMaxSize = 2048; /** Don't read SVG metadata beyond this point. * Default is 1024*256 bytes */ -$wgSVGMetadataCutoff = 262144; +$wgSVGMetadataCutoff = 262144; /** * MediaWiki will reject HTMLesque tags in uploaded files due to idiotic browsers which can't @@ -744,6 +739,12 @@ $wgShowArchiveThumbnails = true; /** Obsolete, always true, kept for compatibility with extensions */ $wgUseImageResize = true; +/** + * If set to true, images that contain certain the exif orientation tag will + * be rotated accordingly. If set to null, try to auto-detect whether a scaler + * is available that can rotate. + */ +$wgEnableAutoRotation = null; /** * Internal name of virus scanner. This servers as a key to the @@ -907,7 +908,7 @@ $wgGalleryOptions = array ( 'imagesPerRow' => 0, // Default number of images per-row in the gallery. 0 -> Adapt to screensize 'imageWidth' => 120, // Width of the cells containing images in galleries (in "px") 'imageHeight' => 120, // Height of the cells containing images in galleries (in "px") - 'captionLength' => 20, // Length of caption to truncate (in characters) + 'captionLength' => 25, // Length of caption to truncate (in characters) 'showBytes' => true, // Show the filesize in bytes in categories ); @@ -976,6 +977,8 @@ $wgDjvuOutputExtension = 'jpg'; * @{ */ +$serverName = substr( $wgServer, strrpos( $wgServer, '/' ) + 1 ); + /** * Site admin email address. */ @@ -1036,6 +1039,11 @@ $wgPasswordReminderResendTime = 24; */ $wgNewPasswordExpiry = 3600 * 24 * 7; +/** + * The time, in seconds, when an email confirmation email expires + */ +$wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60; + /** * SMTP Mode * For using a direct (authenticated) SMTP server connection. @@ -1053,6 +1061,7 @@ $wgSMTP = false; /** * Additional email parameters, will be passed as the last argument to mail() call. + * If using safe_mode this has no effect */ $wgAdditionalMailParams = null; @@ -1177,8 +1186,6 @@ $wgSQLMode = ''; /** Mediawiki schema */ $wgDBmwschema = 'mediawiki'; -/** Tsearch2 schema */ -$wgDBts2schema = 'public'; /** To override default SQLite data directory ($docroot/../data) */ $wgSQLiteDataDir = ''; @@ -1375,6 +1382,7 @@ $wgExternalServers = array(); * * $wgDefaultExternalStore = array( 'DB://cluster1', 'DB://cluster2' ); * + * @var array */ $wgDefaultExternalStore = false; @@ -1467,6 +1475,8 @@ $wgCacheDirectory = false; * - CACHE_DBA: Use PHP's DBA extension to store in a DBM-style * database. This is slow, and is not recommended for * anything other than debugging. + * - (other): A string may be used which identifies a cache + * configuration in $wgObjectCaches. * * @see $wgMessageCacheType, $wgParserCacheType */ @@ -1488,6 +1498,37 @@ $wgMessageCacheType = CACHE_ANYTHING; */ $wgParserCacheType = CACHE_ANYTHING; +/** + * Advanced object cache configuration. + * + * Use this to define the class names and constructor parameters which are used + * for the various cache types. Custom cache types may be defined here and + * referenced from $wgMainCacheType, $wgMessageCacheType or $wgParserCacheType. + * + * The format is an associative array where the key is a cache identifier, and + * the value is an associative array of parameters. The "class" parameter is the + * class name which will be used. Alternatively, a "factory" parameter may be + * given, giving a callable function which will generate a suitable cache object. + * + * The other parameters are dependent on the class used. + */ +$wgObjectCaches = array( + CACHE_NONE => array( 'class' => 'EmptyBagOStuff' ), + CACHE_DB => array( 'class' => 'SqlBagOStuff', 'table' => 'objectcache' ), + CACHE_DBA => array( 'class' => 'DBABagOStuff' ), + + CACHE_ANYTHING => array( 'factory' => 'ObjectCache::newAnything' ), + CACHE_ACCEL => array( 'factory' => 'ObjectCache::newAccelerator' ), + CACHE_MEMCACHED => array( 'factory' => 'ObjectCache::newMemcached' ), + + 'eaccelerator' => array( 'class' => 'eAccelBagOStuff' ), + 'apc' => array( 'class' => 'APCBagOStuff' ), + 'xcache' => array( 'class' => 'XCacheBagOStuff' ), + 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), + 'memcached-php' => array( 'class' => 'MemcachedPhpBagOStuff' ), + 'hash' => array( 'class' => 'HashBagOStuff' ), +); + /** * The expiry time for the parser cache, in seconds. The default is 86.4k * seconds, otherwise known as a day. @@ -1512,7 +1553,7 @@ $wgSessionsInMemcached = false; * 'session_mysql.' Setting to null skips setting this entirely (which might be * useful if you're doing cross-application sessions, see bug 11381) */ -$wgSessionHandler = 'files'; +$wgSessionHandler = null; /** If enabled, will send MemCached debugging information to $wgDebugLogFile */ $wgMemCachedDebug = false; @@ -1544,6 +1585,13 @@ $wgUseLocalMessageCache = false; */ $wgLocalMessageCacheSerialized = true; +/** + * Instead of caching everything, keep track which messages are requested and + * load only most used messages. This only makes sense if there is lots of + * interface messages customised in the wiki (like hundreds in many languages). + */ +$wgAdaptiveMessageCache = false; + /** * Localisation cache configuration. Associative array with keys: * class: The class to use. May be overridden by extensions. @@ -1589,7 +1637,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches do not keep obsolete copies of global * styles. */ -$wgStyleVersion = '301'; +$wgStyleVersion = '303'; /** * This will cache static pages for non-logged-in users to reduce @@ -1673,9 +1721,9 @@ $wgClockSkewFudge = 5; * to setting $wgCacheEpoch to the modification time of LocalSettings.php, as * was previously done in the default LocalSettings.php file. * - * On high-traffic wikis, this should be set to false, to avoid the need to + * On high-traffic wikis, this should be set to false, to avoid the need to * check the file modification time, and to avoid the performance impact of - * unnecessary cache invalidations. + * unnecessary cache invalidations. */ $wgInvalidateCacheOnLocalSettingsChange = true; @@ -1706,13 +1754,22 @@ $wgUseESI = false; /** Send X-Vary-Options header for better caching (requires patched Squid) */ $wgUseXVO = false; +/** Add X-Forwarded-Proto to the Vary and X-Vary-Options headers for API + * requests and RSS/Atom feeds. Use this if you have an SSL termination setup + * and need to split the cache between HTTP and HTTPS for API requests, + * feed requests and HTTP redirect responses in order to prevent cache + * pollution. This does not affect 'normal' requests to index.php other than + * HTTP redirects. + */ +$wgVaryOnXFP = false; + /** * Internal server name as known to Squid, if different. Example: * * $wgInternalServer = 'http://yourinternal.tld:8000'; * */ -$wgInternalServer = $wgServer; +$wgInternalServer = false; /** * Cache timeout for the squid, will be sent as s-maxage (without ESI) or @@ -1807,20 +1864,15 @@ $wgDummyLanguageCodes = array( 'als', 'bat-smg', 'be-x-old', - 'dk', 'fiu-vro', 'iu', 'nb', 'qqq', + 'qqx', + 'roa-rup', 'simple', - 'tp', ); -/** @deprecated Since MediaWiki 1.5, this must always be set to UTF-8. */ -$wgInputEncoding = 'UTF-8'; -/** @deprecated Since MediaWiki 1.5, this must always be set to UTF-8. */ -$wgOutputEncoding = 'UTF-8'; - /** * Character set for use in the article edit box. Language-specific encodings * may be defined. @@ -2060,17 +2112,7 @@ $wgLocaltimezone = null; * This setting is used for most date/time displays in the software, and is * overrideable in user preferences. It is *not* used for signature timestamps. * - * You can set it to match the configured server timezone like this: - * $wgLocalTZoffset = date("Z") / 60; - * - * If your server is not configured for the timezone you want, you can set - * this in conjunction with the signature timezone and override the PHP default - * timezone like so: - * $wgLocaltimezone="Europe/Berlin"; - * date_default_timezone_set( $wgLocaltimezone ); - * $wgLocalTZoffset = date("Z") / 60; - * - * Leave at NULL to show times in universal time (UTC/GMT). + * By default, this will be set to match $wgLocaltimezone. */ $wgLocalTZoffset = null; @@ -2084,28 +2126,49 @@ $wgLocalTZoffset = null; /** The default Content-Type header. */ $wgMimeType = 'text/html'; -/** The content type used in script tags. */ +/** + * The content type used in script tags. This is mostly going to be ignored if + * $wgHtml5 is true, at least for actual HTML output, since HTML5 doesn't + * require a MIME type for JavaScript or CSS (those are the default script and + * style languages). + */ $wgJsMimeType = 'text/javascript'; -/** The HTML document type. */ +/** + * The HTML document type. Ignored if $wgHtml5 is true, since + * doesn't actually have a doctype part to put this variable's contents in. + */ $wgDocType = '-//W3C//DTD XHTML 1.0 Transitional//EN'; -/** The URL of the document type declaration. */ +/** + * The URL of the document type declaration. Ignored if $wgHtml5 is true, + * since HTML5 has no DTD, and doesn't actually have a DTD part + * to put this variable's contents in. + */ $wgDTD = 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'; -/** The default xmlns attribute. */ +/** + * The default xmlns attribute. Ignored if $wgHtml5 is true (or it's supposed + * to be), since we don't currently support XHTML5, and in HTML5 (i.e., served + * as text/html) the attribute has no effect, so why bother? + */ $wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml'; /** * Should we output an HTML5 doctype? If false, use XHTML 1.0 Transitional * instead, and disable HTML5 features. This may eventually be removed and set - * to always true. + * to always true. If it's true, a number of other settings will be irrelevant + * and have no effect. */ $wgHtml5 = true; /** * Defines the value of the version attribute in the <html> tag, if any. - * Will be initialized later if not set explicitly. + * This is ignored if $wgHtml5 is false. If $wgAllowRdfaAttributes and + * $wgHtml5 are both true, and this evaluates to boolean false (like if it's + * left at the default null value), it will be auto-initialized to the correct + * value for RDFa+HTML5. As such, you should have no reason to ever actually + * set this to anything. */ $wgHtml5Version = null; @@ -2145,6 +2208,9 @@ $wgWellFormedXml = true; * $wgXhtmlNamespaces['svg'] = 'http://www.w3.org/2000/svg'; * Normally we wouldn't have to define this in the root * element, but IE needs it there in some circumstances. + * + * This is ignored if $wgHtml5 is true, for the same reason as + * $wgXhtmlDefaultNamespace. */ $wgXhtmlNamespaces = array(); @@ -2187,12 +2253,6 @@ $wgValidateAllHtml = false; */ $wgDefaultSkin = 'vector'; -/** -* Should we allow the user's to select their own skin that will override the default? -* @deprecated in 1.16, use $wgHiddenPrefs[] = 'skin' to disable it -*/ -$wgAllowUserSkin = true; - /** * Specify the name of a skin that should not be presented in the list of * available skins. Use for blacklisting a skin which you do not want to @@ -2269,7 +2329,7 @@ $wgEnableTooltipsAndAccesskeys = true; $wgBreakFrames = false; /** - * The X-Frame-Options header to send on pages sensitive to clickjacking + * The X-Frame-Options header to send on pages sensitive to clickjacking * attacks, such as edit pages. This prevents those pages from being displayed * in a frame or iframe. The options are: * @@ -2279,9 +2339,9 @@ $wgBreakFrames = false; * to allow framing within a trusted domain. This is insecure if there * is a page on the same domain which allows framing of arbitrary URLs. * - * - false: Allow all framing. This opens up the wiki to XSS attacks and thus - * full compromise of local user accounts. Private wikis behind a - * corporate firewall are especially vulnerable. This is not + * - false: Allow all framing. This opens up the wiki to XSS attacks and thus + * full compromise of local user accounts. Private wikis behind a + * corporate firewall are especially vulnerable. This is not * recommended. * * For extra safety, set $wgBreakFrames = true, to prevent framing on all pages, @@ -2310,17 +2370,17 @@ $wgExperimentalHtmlIds = false; * You can add new icons to the built in copyright or poweredby, or you can create * a new block. Though note that you may need to add some custom css to get good styling * of new blocks in monobook. vector and modern should work without any special css. - * + * * $wgFooterIcons itself is a key/value array. - * The key is the name of a block that the icons will be wrapped in. The final id varies - * by skin; Monobook and Vector will turn poweredby into f-poweredbyico while Modern + * The key is the name of a block that the icons will be wrapped in. The final id varies + * by skin; Monobook and Vector will turn poweredby into f-poweredbyico while Modern * turns it into mw_poweredby. * The value is either key/value array of icons or a string. * In the key/value array the key may or may not be used by the skin but it can * be used to find the icon and unset it or change the icon if needed. * This is useful for disabling icons that are set by extensions. - * The value should be either a string or an array. If it is a string it will be output - * directly as html, however some skins may choose to ignore it. An array is the preferred format + * The value should be either a string or an array. If it is a string it will be output + * directly as html, however some skins may choose to ignore it. An array is the preferred format * for the icon, the following keys are used: * src: An absolute url to the image to use for the icon, this is recommended * but not required, however some skins will ignore icons without an image @@ -2345,6 +2405,13 @@ $wgFooterIcons = array( ), ); +/** + * Login / create account link behavior when it's possible for anonymous users to create an account + * true = use a combined login / create account link + * false = split login and create account into two separate links + */ +$wgUseCombinedLoginLink = true; + /** * Search form behavior for Vector skin only * true = use an icon search button @@ -2370,9 +2437,12 @@ $wgVectorShowVariantName = false; $wgEdititis = false; /** - * Experimental better directionality support. + * Better directionality support (bug 6100 and related). + * Removed in 1.18, still kept here for LiquidThreads backwards compatibility. + * + * @deprecated since 1.18 */ -$wgBetterDirectionality = false; +$wgBetterDirectionality = true; /** @} */ # End of output format settings } @@ -2397,6 +2467,12 @@ $wgBetterDirectionality = false; */ $wgResourceModules = array(); +/* + * Default 'remoteBasePath' value for resource loader modules. + * If not set, then $wgScriptPath will be used as a fallback. + */ +$wgResourceBasePath = null; + /** * Maximum time in seconds to cache resources served by the resource loader */ @@ -2452,6 +2528,19 @@ $wgResourceLoaderMinifierMaxLineLength = 1000; */ $wgIncludeLegacyJavaScript = true; +/** + * Whether or not to assing configuration variables to the global window object. + * If this is set to false, old code using deprecated variables like: + * " if ( window.wgRestrictionEdit ) ..." + * or: + * " if ( wgIsArticle ) ..." + * will no longer work and needs to use mw.config instead. For example: + * " if ( mw.config.exists('wgRestrictionEdit') )" + * or + * " if ( mw.config.get('wgIsArticle') )". + */ +$wgLegacyJavaScriptGlobals = true; + /** * If set to a positive number, ResourceLoader will not generate URLs whose * query string is more than this many characters long, and will instead use @@ -2465,6 +2554,25 @@ $wgIncludeLegacyJavaScript = true; */ $wgResourceLoaderMaxQueryLength = -1; +/** + * If set to true, JavaScript modules loaded from wiki pages will be parsed prior + * to minification to validate it. + * + * Parse errors will result in a JS exception being thrown during module load, + * which avoids breaking other modules loaded in the same request. + */ +$wgResourceLoaderValidateJS = true; + +/** + * If set to true, statically-sourced (file-backed) JavaScript resources will + * be parsed for validity before being bundled up into ResourceLoader modules. + * + * This can be helpful for development by providing better error messages in + * default (non-debug) mode, but JavaScript parsing is slow and memory hungry + * and may fail on large pre-bundled frameworks. + */ +$wgResourceLoaderValidateStaticJS = false; + /** @} */ # End of resource loader settings } @@ -2510,6 +2618,14 @@ $wgMetaNamespaceTalk = false; # ); $wgExtraNamespaces = array(); +/** + * Same as above, but for namespaces with gender distinction. + * Note: the default form for the namespace should also be set + * using $wgExtraNamespaces for the same index. + * @since 1.18 + */ +$wgExtraGenderNamespaces = array(); + /** * Namespace aliases * These are alternate names for the primary localised namespace names, which @@ -2720,6 +2836,7 @@ $wgUrlProtocols = array( 'https://', 'ftp://', 'irc://', + 'ircs://', // @bug 28503 'gopher://', 'telnet://', // Well if we're going to support the above.. -ævar 'nntp://', // @bug 3808 RFC 1738 @@ -2729,6 +2846,7 @@ $wgUrlProtocols = array( 'svn://', 'git://', 'mms://', + '//', // for protocol-relative URLs ); /** @@ -2781,8 +2899,9 @@ $wgAllowImageTag = false; * - $wgTidyBin should be set to the path of the binary and * - $wgTidyConf to the path of the configuration file. * - $wgTidyOpts can include any number of parameters. - * - $wgTidyInternal controls the use of the PECL extension to use an in- - * process tidy library instead of spawning a separate program. + * - $wgTidyInternal controls the use of the PECL extension or the + * libtidy (PHP >= 5) extension to use an in-process tidy library instead + * of spawning a separate program. * Normally you shouldn't need to override the setting except for * debugging. To install, use 'pear install tidy' and add a line * 'extension=tidy.so' to php.ini. @@ -2862,6 +2981,7 @@ $wgExpensiveParserFunctionLimit = 100; /** * Preprocessor caching threshold + * Setting it to 'false' will disable the preprocessor cache. */ $wgPreprocessorCacheThreshold = 1000; @@ -2883,14 +3003,30 @@ $wgTranscludeCacheExpiry = 3600; */ /** - * Under which condition should a page in the main namespace be counted - * as a valid article? If $wgUseCommaCount is set to true, it will be - * counted if it contains at least one comma. If it is set to false - * (default), it will only be counted if it contains at least one [[wiki - * link]]. See http://www.mediawiki.org/wiki/Manual:Article_count + * Method used to determine if a page in a content namespace should be counted + * as a valid article. + * + * Redirect pages will never be counted as valid articles. * - * Retroactively changing this variable will not affect - * the existing count (cf. maintenance/recount.sql). + * This variable can have the following values: + * - 'any': all pages as considered as valid articles + * - 'comma': the page must contain a comma to be considered valid + * - 'link': the page must contain a [[wiki link]] to be considered valid + * - null: the value will be set at run time depending on $wgUseCommaCount: + * if $wgUseCommaCount is false, it will be 'link', if it is true + * it will be 'comma' + * + * See also See http://www.mediawiki.org/wiki/Manual:Article_count + * + * Retroactively changing this variable will not affect the existing count, + * to update it, you will need to run the maintenance/updateArticleCount.php + * script. + */ +$wgArticleCountMethod = null; + +/** + * Backward compatibility setting, will set $wgArticleCountMethod if it is null. + * @deprecated since 1.18; use $wgArticleCountMethod instead */ $wgUseCommaCount = false; @@ -2927,6 +3063,17 @@ $wgPasswordSalt = true; */ $wgMinimalPasswordLength = 1; +/** + * Whether to allow password resets ("enter some identifying data, and we'll send an email + * with a temporary password you can use to get back into the account") identified by + * various bits of data. Setting all of these to false (or the whole variable to false) + * has the effect of disabling password resets entirely + */ +$wgPasswordResetRoutes = array( + 'username' => true, + 'email' => false, +); + /** * Maximum number of Unicode characters in signature */ @@ -2962,8 +3109,6 @@ $wgReservedUsernames = array( $wgDefaultUserOptions = array( 'ccmeonemails' => 0, 'cols' => 80, - 'contextchars' => 50, - 'contextlines' => 5, 'date' => 'default', 'diffonly' => 0, 'disablemail' => 0, @@ -2996,7 +3141,7 @@ $wgDefaultUserOptions = array( 'numberheadings' => 0, 'previewonfirst' => 0, 'previewontop' => 1, - 'quickbar' => 1, + 'quickbar' => 5, 'rcdays' => 7, 'rclimit' => 50, 'rememberpassword' => 0, @@ -3029,7 +3174,7 @@ $wgDefaultUserOptions = array( /** * Whether or not to allow and use real name fields. - * @deprecated in 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real + * @deprecated since 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real * names */ $wgAllowRealName = true; @@ -3127,18 +3272,6 @@ $wgSecureLogin = false; * @{ */ -/** - * Allow sysops to ban logged-in users - * @deprecated since 1.17, will be made permanently true in 1.18 - */ -$wgSysopUserBans = true; - -/** - * Allow sysops to ban IP ranges - * @deprecated since 1.17; set $wgBlockCIDRLimit to array( 'IPv4' => 32, 'IPv6 => 128 ) instead. - */ -$wgSysopRangeBans = true; - /** * Number of seconds before autoblock entries expire. Default 86400 = 1 day. */ @@ -3180,7 +3313,7 @@ $wgBlockDisablesLogin = false; * $wgWhitelistRead = array ( "Main Page", "Wikipedia:Help"); * * - * Special:Userlogin and Special:Resetpass are always whitelisted. + * Special:Userlogin and Special:ChangePassword are always whitelisted. * * NOTE: This will only work if $wgGroupPermissions['*']['read'] is false -- * see below. Otherwise, ALL pages are accessible, regardless of this setting. @@ -3278,7 +3411,6 @@ $wgGroupPermissions['sysop']['autopatrol'] = true; $wgGroupPermissions['sysop']['protect'] = true; $wgGroupPermissions['sysop']['proxyunbannable'] = true; $wgGroupPermissions['sysop']['rollback'] = true; -$wgGroupPermissions['sysop']['trackback'] = true; $wgGroupPermissions['sysop']['upload'] = true; $wgGroupPermissions['sysop']['reupload'] = true; $wgGroupPermissions['sysop']['reupload-shared'] = true; @@ -3295,6 +3427,7 @@ $wgGroupPermissions['sysop']['movefile'] = true; $wgGroupPermissions['sysop']['unblockself'] = true; $wgGroupPermissions['sysop']['suppressredirect'] = true; #$wgGroupPermissions['sysop']['mergehistory'] = true; +#$wgGroupPermissions['sysop']['trackback'] = true; // Permission to change users' group assignments $wgGroupPermissions['bureaucrat']['userrights'] = true; @@ -3364,7 +3497,7 @@ $wgGroupsRemoveFromSelf = array(); * Set of available actions that can be restricted via action=protect * You probably shouldn't change this. * Translated through restriction-* messages. - * Title::getRestrictionTypes() will remove restrictions that are not + * Title::getRestrictionTypes() will remove restrictions that are not * applicable to a specific title (create and upload) */ $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ); @@ -3429,7 +3562,7 @@ $wgAutoConfirmCount = 0; /** * Automatically add a usergroup to any user who matches certain conditions. * The format is - * array( '&' or '|' or '^', cond1, cond2, ... ) + * array( '&' or '|' or '^' or '!', cond1, cond2, ... ) * where cond1, cond2, ... are themselves conditions; *OR* * APCOND_EMAILCONFIRMED, *OR* * array( APCOND_EMAILCONFIRMED ), *OR* @@ -3440,6 +3573,7 @@ $wgAutoConfirmCount = 0; * array( APCOND_IPINRANGE, range ), *OR* * array( APCOND_AGE_FROM_EDIT, seconds since first edit ), *OR* * array( APCOND_BLOCKED ), *OR* + * array( APCOND_ISBOT ), *OR* * similar constructs defined by extensions. * * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any @@ -3452,6 +3586,31 @@ $wgAutopromote = array( ), ); +/** + * Automatically add a usergroup to any user who matches certain conditions. + * Does not add the user to the group again if it has been removed. + * Also, does not remove the group if the user no longer meets the criteria. + * + * The format is + * array( event => criteria, ... ) + * where event is + * 'onEdit' (when user edits) or 'onView' (when user views the wiki) + * and criteria has the same format as $wgAutopromote + * + * @see $wgAutopromote + * @since 1.18 + */ +$wgAutopromoteOnce = array( + 'onEdit' => array(), + 'onView' => array() +); + +/* + * Put user rights log entries for autopromotion in recent changes? + * @since 1.18 + */ +$wgAutopromoteOnceLogInRC = true; + /** * $wgAddGroups and $wgRemoveGroups can be used to give finer control over who * can assign which groups at Special:Userrights. Example configuration: @@ -3512,7 +3671,7 @@ $wgSummarySpamRegex = array(); * - true : block it * - false : let it through * - * @deprecated Use hooks. See SpamBlacklist extension. + * @deprecated since 1.17 Use hooks. See SpamBlacklist extension. */ $wgFilterCallback = false; @@ -3523,7 +3682,7 @@ $wgFilterCallback = false; $wgEnableDnsBlacklist = false; /** - * @deprecated Use $wgEnableDnsBlacklist instead, only kept for backward + * @deprecated since 1.17 Use $wgEnableDnsBlacklist instead, only kept for backward * compatibility */ $wgEnableSorbs = false; @@ -3535,7 +3694,7 @@ $wgEnableSorbs = false; $wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' ); /** - * @deprecated Use $wgDnsBlacklistUrls instead, only kept for backward + * @deprecated since 1.17 Use $wgDnsBlacklistUrls instead, only kept for backward * compatibility */ $wgSorbsUrl = array(); @@ -3582,17 +3741,6 @@ $wgRateLimits = array( */ $wgRateLimitLog = null; -/** - * Array of groups which should never trigger the rate limiter - * - * @deprecated as of 1.13.0, the preferred method is using - * $wgGroupPermissions[]['noratelimit']. However, this will still - * work if desired. - * - * $wgRateLimitsExcludedGroups = array( 'sysop', 'bureaucrat' ); - */ -$wgRateLimitsExcludedGroups = array(); - /** * Array of IPs which should be excluded from rate limits. * This may be useful for whitelisting NAT gateways for conferences, etc. @@ -3631,7 +3779,7 @@ $wgBlockOpenProxies = false; /** Port we want to scan for a proxy */ $wgProxyPorts = array( 80, 81, 1080, 3128, 6588, 8000, 8080, 8888, 65506 ); /** Script used to scan */ -$wgProxyScriptPath = "$IP/includes/proxy_check.php"; +$wgProxyScriptPath = "$IP/maintenance/proxy_check.php"; /** */ $wgProxyMemcExpiry = 86400; /** This should always be customised in LocalSettings.php */ @@ -3658,13 +3806,34 @@ $wgCookieExpiration = 30*86400; * or ".any.subdomain.net" */ $wgCookieDomain = ''; + + +/** + * Set this variable if you want to restrict cookies to a certain path within + * the domain specified by $wgCookieDomain. + */ $wgCookiePath = '/'; -$wgCookieSecure = ($wgProto == 'https'); + +/** + * Whether the "secure" flag should be set on the cookie. This can be: + * - true: Set secure flag + * - false: Don't set secure flag + * - "detect": Set the secure flag if $wgServer is set to an HTTPS URL + */ +$wgCookieSecure = 'detect'; + +/** + * By default, MediaWiki checks if the client supports cookies during the + * login process, so that it can display an informative error message if + * cookies are disabled. Set this to true if you want to disable this cookie + * check. + */ $wgDisableCookieCheck = false; /** - * Set $wgCookiePrefix to use a custom one. Setting to false sets the default of - * using the database name. + * Cookies generated by MediaWiki have names starting with this prefix. Set it + * to a string to use a custom prefix. Setting it to false causes the database + * name to be used as a prefix. */ $wgCookiePrefix = false; @@ -3672,10 +3841,8 @@ $wgCookiePrefix = false; * Set authentication cookies to HttpOnly to prevent access by JavaScript, * in browsers that support this feature. This can mitigates some classes of * XSS attack. - * - * Only supported on PHP 5.2 or higher. */ -$wgCookieHttpOnly = version_compare("5.2", PHP_VERSION, "<"); +$wgCookieHttpOnly = true; /** * If the requesting browser matches a regex in this blacklist, we won't @@ -3708,28 +3875,6 @@ $wgSessionName = false; * Please see math/README for more information. */ $wgUseTeX = false; -/** Location of the texvc binary */ -$wgTexvc = $IP . '/math/texvc'; -/** - * Texvc background color - * use LaTeX color format as used in \special function - * for transparent background use value 'Transparent' for alpha transparency or - * 'transparent' for binary transparency. - */ -$wgTexvcBackgroundColor = 'transparent'; - -/** - * Normally when generating math images, we double-check that the - * directories we want to write to exist, and that files that have - * been generated still exist when we need to bring them up again. - * - * This lets us give useful error messages in case of permission - * problems, and automatically rebuild images that have been lost. - * - * On a big site with heavy NFS traffic this can be slow and flaky, - * so sometimes we want to short-circuit it by setting this to false. - */ -$wgMathCheckFiles = true; /* @} */ # end LaTeX } @@ -3761,7 +3906,7 @@ $wgDebugLogPrefix = ''; $wgDebugRedirects = false; /** - * If true, log debugging data from action=raw. + * If true, log debugging data from action=raw and load.php. * This is normally false to avoid overlapping debug entries due to gen=css and * gen=js requests. */ @@ -3885,7 +4030,7 @@ $wgDebugProfiling = false; /** Output debug message on every wfProfileIn/wfProfileOut */ $wgDebugFunctionEntry = 0; -/* +/** * Destination for wfIncrStats() data... * 'cache' to go into the system cache, if enabled (memcached) * 'udp' to be sent to the UDP profiler (see $wgUDPProfilerHost) @@ -3893,6 +4038,14 @@ $wgDebugFunctionEntry = 0; */ $wgStatsMethod = 'cache'; +/** + * When $wgStatsMethod is 'udp', setting this to a string allows statistics to + * be aggregated over more than one wiki. The string will be used in place of + * the DB name in outgoing UDP packets. If this is set to false, the DB name + * will be used. + */ +$wgAggregateStatsID = false; + /** Whereas to count the number of time an article is viewed. * Does not work if pages are cached (for example with squid). */ @@ -3901,6 +4054,8 @@ $wgDisableCounters = false; /** * Support blog-style "trackbacks" for articles. See * http://www.sixapart.com/pronet/docs/trackback_spec for details. + * + * If enabling this, you also need to grant the 'trackback' right to a group */ $wgUseTrackbacks = false; @@ -3914,8 +4069,8 @@ $wgUseTrackbacks = false; * Use full paths. */ $wgParserTestFiles = array( - "$IP/maintenance/tests/parser/parserTests.txt", - "$IP/maintenance/tests/parser/ExtraParserTests.txt" + "$IP/tests/parser/parserTests.txt", + "$IP/tests/parser/extraParserTests.txt" ); /** @@ -3954,11 +4109,8 @@ $wgAdvancedSearchHighlighting = false; /** * Regexp to match word boundaries, defaults for non-CJK languages * should be empty for CJK since the words are not separate - * - * @todo FIXME: checks for lower than required PHP version (5.1.x). */ -$wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z}\p{P}\p{C}]' - : '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\n\r\/{}]'; // PHP 5.0 workaround +$wgSearchHighlightBoundaries = '[\p{Z}\p{P}\p{C}]'; /** * Set to true to have the search engine count total @@ -4176,12 +4328,12 @@ $wgReadOnly = null; $wgReadOnlyFile = false; /** - * When you run the web-based upgrade utility, it will tell you what to set + * When you run the web-based upgrade utility, it will tell you what to set * this to in order to authorize the upgrade process. It will subsequently be * used as a password, to authorize further upgrades. * - * For security, do not set this to a guessable string. Use the value supplied - * by the install/upgrade process. To cause the upgrader to generate a new key, + * For security, do not set this to a guessable string. Use the value supplied + * by the install/upgrade process. To cause the upgrader to generate a new key, * delete the old key from LocalSettings.php. */ $wgUpgradeKey = false; @@ -4288,6 +4440,16 @@ $wgFeedDiffCutoff = 32768; */ $wgOverrideSiteFeed = array(); +/** + * Available feeds objects + * Should probably only be defined when a page is syndicated ie when + * $wgOut->isSyndicated() is true + */ +$wgFeedClasses = array( + 'rss' => 'RSSFeed', + 'atom' => 'AtomFeed', +); + /** * Which feed types should we provide by default? This can include 'rss', * 'atom', neither, or both. @@ -4337,16 +4499,31 @@ $wgUseTagFilter = true; * @{ */ -/** RDF metadata toggles */ -$wgEnableDublinCoreRdf = false; -$wgEnableCreativeCommonsRdf = false; - -/** Override for copyright metadata. - * TODO: these options need documentation +/** + * Override for copyright metadata. + * + * This is the name of the page containing information about the wiki's copyright status, + * which will be added as a link in the footer if it is specified. It overrides + * $wgRightsUrl if both are specified. */ $wgRightsPage = null; + +/** + * Set this to specify an external URL containing details about the content license used on your wiki. + * If $wgRightsPage is set then this setting is ignored. + */ $wgRightsUrl = null; + +/** + * If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link. + * If using $wgRightsUrl then this value must be specified. If using $wgRightsPage then the name of the + * page will also be used as the link if this variable is not set. + */ $wgRightsText = null; + +/** + * Override for copyright metadata. + */ $wgRightsIcon = null; /** @@ -4356,17 +4533,13 @@ $wgLicenseTerms = false; /** * Set this to some HTML to override the rights icon with an arbitrary logo - * @deprecated Use $wgFooterIcons['copyright']['copyright'] + * @deprecated since 1.18 Use $wgFooterIcons['copyright']['copyright'] */ $wgCopyrightIcon = null; /** Set this to true if you want detailed copyright information forms on Upload. */ $wgUseCopyrightUpload = false; -/** Set this to false if you want to disable checking that detailed copyright - * information values are not empty. */ -$wgCheckCopyrightUpload = true; - /** * Set this to the number of authors that you want to be credited below an * article text. Set it to zero to hide the attribution block, and a negative @@ -4455,12 +4628,6 @@ $wgExportFromNamespaces = false; */ $wgExtensionFunctions = array(); -/** - * Extension functions for initialisation of skins. This is called somewhat earlier - * than $wgExtensionFunctions. - */ -$wgSkinExtensionFunctions = array(); - /** * Extension messages files. * @@ -4480,7 +4647,7 @@ $wgExtensionMessagesFiles = array(); /** * Aliases for special pages provided by extensions. - * @deprecated Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles + * @deprecated since 1.16 Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles */ $wgExtensionAliasesFiles = array(); @@ -4500,7 +4667,10 @@ $wgParserOutputHooks = array(); /** * List of valid skin names. - * The key should be the name in all lower case, the value should be a display name. + * The key should be the name in all lower case, the value should be a properly + * cased name for the skin. This value will be prefixed with "Skin" to create the + * class name of the skin to load, and if the skin's class cannot be found through + * the autoloader it will be used to load a .php file by that name in the skins directory. * The default skins will be added later, by Skin::getSkinNames(). Use * Skin::getSkinNames() as an accessor if you wish to have access to the full list. */ @@ -4542,6 +4712,7 @@ $wgExtensionCredits = array(); /** * Authentication plugin. + * @var AuthPlugin */ $wgAuth = null; @@ -4571,6 +4742,17 @@ $wgJobClasses = array( 'uploadFromUrl' => 'UploadFromUrlJob', ); +/** + * Jobs that must be explicitly requested, i.e. aren't run by job runners unless special flags are set. + * + * These can be: + * - Very long-running jobs. + * - Jobs that you would never want to run as part of a page rendering request. + * - Jobs that you want to run on specialized machines ( like transcoding, or a particular + * machine on your cluster has 'outside' web access you could restrict uploadFromUrl ) + */ +$wgJobTypesExcludedFromDefaultQueue = array(); + /** * Additional functions to be performed with updateSpecialPages. * Expensive Querypages are already updated. @@ -4624,24 +4806,29 @@ $wgCategoryMagicGallery = true; $wgCategoryPagingLimit = 200; /** - * Specify how category names should be sorted, when listed on a category page. + * Specify how category names should be sorted, when listed on a category page. * A sorting scheme is also known as a collation. * * Available values are: * * - uppercase: Converts the category name to upper case, and sorts by that. * - * - uca-default: Provides access to the Unicode Collation Algorithm with + * - identity: Does no conversion. Sorts by binary value of the string. + * + * - uca-default: Provides access to the Unicode Collation Algorithm with * the default element table. This is a compromise collation which sorts * all languages in a mediocre way. However, it is better than "uppercase". * - * To use the uca-default collation, you must have PHP's intl extension - * installed. See http://php.net/manual/en/intl.setup.php . The details of the - * resulting collation will depend on the version of ICU installed on the + * To use the uca-default collation, you must have PHP's intl extension + * installed. See http://php.net/manual/en/intl.setup.php . The details of the + * resulting collation will depend on the version of ICU installed on the * server. * * After you change this, you must run maintenance/updateCollation.php to fix - * the sort keys in the database. + * the sort keys in the database. + * + * Extensions can define there own collations by subclassing Collation + * and using the Collation::factory hook. */ $wgCategoryCollation = 'uppercase'; @@ -4753,34 +4940,34 @@ $wgLogHeaders = array( * Extensions with custom log types may add to this array. */ $wgLogActions = array( - 'block/block' => 'blocklogentry', - 'block/unblock' => 'unblocklogentry', - 'block/reblock' => 'reblock-logentry', - 'protect/protect' => 'protectedarticle', - 'protect/modify' => 'modifiedarticleprotection', - 'protect/unprotect' => 'unprotectedarticle', - 'protect/move_prot' => 'movedarticleprotection', - 'rights/rights' => 'rightslogentry', - 'rights/disable' => 'disableaccount-logentry', - 'delete/delete' => 'deletedarticle', - 'delete/restore' => 'undeletedarticle', - 'delete/revision' => 'revdelete-logentry', - 'delete/event' => 'logdelete-logentry', - 'upload/upload' => 'uploadedimage', - 'upload/overwrite' => 'overwroteimage', - 'upload/revert' => 'uploadedimage', - 'move/move' => '1movedto2', - 'move/move_redir' => '1movedto2_redir', - 'import/upload' => 'import-logentry-upload', - 'import/interwiki' => 'import-logentry-interwiki', - 'merge/merge' => 'pagemerge-logentry', - 'suppress/revision' => 'revdelete-logentry', - 'suppress/file' => 'revdelete-logentry', - 'suppress/event' => 'logdelete-logentry', - 'suppress/delete' => 'suppressedarticle', - 'suppress/block' => 'blocklogentry', - 'suppress/reblock' => 'reblock-logentry', - 'patrol/patrol' => 'patrol-log-line', + 'block/block' => 'blocklogentry', + 'block/unblock' => 'unblocklogentry', + 'block/reblock' => 'reblock-logentry', + 'protect/protect' => 'protectedarticle', + 'protect/modify' => 'modifiedarticleprotection', + 'protect/unprotect' => 'unprotectedarticle', + 'protect/move_prot' => 'movedarticleprotection', + 'rights/rights' => 'rightslogentry', + 'rights/autopromote' => 'rightslogentry-autopromote', + 'delete/delete' => 'deletedarticle', + 'delete/restore' => 'undeletedarticle', + 'delete/revision' => 'revdelete-logentry', + 'delete/event' => 'logdelete-logentry', + 'upload/upload' => 'uploadedimage', + 'upload/overwrite' => 'overwroteimage', + 'upload/revert' => 'uploadedimage', + 'move/move' => '1movedto2', + 'move/move_redir' => '1movedto2_redir', + 'import/upload' => 'import-logentry-upload', + 'import/interwiki' => 'import-logentry-interwiki', + 'merge/merge' => 'pagemerge-logentry', + 'suppress/revision' => 'revdelete-logentry', + 'suppress/file' => 'revdelete-logentry', + 'suppress/event' => 'logdelete-logentry', + 'suppress/delete' => 'suppressedarticle', + 'suppress/block' => 'blocklogentry', + 'suppress/reblock' => 'reblock-logentry', + 'patrol/patrol' => 'patrol-log-line', ); /** @@ -4795,11 +4982,6 @@ $wgLogActionsHandlers = array(); */ $wgNewUserLog = true; -/** - * Log the automatic creations of new users accounts? - */ -$wgLogAutocreatedAccounts = false; - /** @} */ # end logging } /*************************************************************************//** @@ -4868,16 +5050,18 @@ $wgSpecialPageGroups = array( 'Listusers' => 'users', 'Activeusers' => 'users', 'Listgrouprights' => 'users', - 'Ipblocklist' => 'users', + 'BlockList' => 'users', 'Contributions' => 'users', 'Emailuser' => 'users', 'Listadmins' => 'users', 'Listbots' => 'users', 'Userrights' => 'users', - 'Blockip' => 'users', + 'Block' => 'users', + 'Unblock' => 'users', 'Preferences' => 'users', - 'Resetpass' => 'users', + 'ChangePassword' => 'users', 'DeletedContributions' => 'users', + 'PasswordReset' => 'users', 'Mostlinked' => 'highuse', 'Mostlinkedcategories' => 'highuse', @@ -4925,12 +5109,6 @@ $wgSpecialPageGroups = array( $wgSortSpecialPages = true; -/** - * Filter for Special:Randompage. Part of a WHERE clause - * @deprecated as of 1.16, use the SpecialRandomGetRandomTitle hook - */ -$wgExtraRandompageSQL = false; - /** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. @@ -4945,6 +5123,47 @@ $wgMaxRedirectLinksRetrieved = 500; /** @} */ # end special pages } +/*************************************************************************//** + * @name Actions + * @{ + */ + +/** + * Array of allowed values for the title=foo&action= parameter. Syntax is: + * 'foo' => 'ClassName' Load the specified class which subclasses Action + * 'foo' => true Load the class FooAction which subclasses Action + * If something is specified in the getActionOverrides() + * of the relevant Page object it will be used + * instead of the default class. + * 'foo' => false The action is disabled; show an error message + * Unsetting core actions will probably cause things to complain loudly. + */ +$wgActions = array( + 'credits' => true, + 'deletetrackback' => true, + 'info' => true, + 'markpatrolled' => true, + 'purge' => true, + 'revert' => true, + 'revisiondelete' => true, + 'rollback' => true, + 'unwatch' => true, + 'watch' => true, +); + +/** + * Array of disabled article actions, e.g. view, edit, delete, etc. + * @deprecated since 1.18; just set $wgActions['action'] = false instead + */ +$wgDisabledActions = array(); + +/** + * Allow the "info" action, very inefficient at the moment + */ +$wgAllowPageInfo = false; + +/** @} */ # end actions } + /*************************************************************************//** * @name Robot (search engine crawler) policy * See also $wgNoFollowLinks. @@ -5060,14 +5279,7 @@ $wgAPIMaxUncachedDiffs = 1; $wgAPIRequestLog = false; /** - * Cache the API help text for up to an hour. Disable this during API - * debugging and development - */ -$wgAPICacheHelp = true; - -/** - * Set the timeout for the API help text cache. Ignored if $wgAPICacheHelp - * is false. + * Set the timeout for the API help text cache. If set to 0, caching disabled */ $wgAPICacheHelpTimeout = 60*60; @@ -5080,12 +5292,11 @@ $wgUseAjax = true; * List of Ajax-callable functions. * Extensions acting as Ajax callbacks must register here */ -$wgAjaxExportList = array( 'wfAjaxGetFileUrl' ); +$wgAjaxExportList = array(); /** * Enable watching/unwatching pages using AJAX. * Requires $wgUseAjax to be true too. - * Causes wfAjaxWatch to be added to $wgAjaxExportList */ $wgAjaxWatch = true; @@ -5214,20 +5425,62 @@ $wgUpdateRowsPerQuery = 100; /** @} */ # End job queue } /************************************************************************//** - * @name Miscellaneous + * @name HipHop compilation * @{ */ -/** Allow the "info" action, very inefficient at the moment */ -$wgAllowPageInfo = false; +/** + * The build directory for HipHop compilation. + * Defaults to $IP/maintenance/hiphop/build. + */ +$wgHipHopBuildDirectory = false; -/** Name of the external diff engine to use */ -$wgExternalDiffEngine = false; +/** + * The HipHop build type. Can be either "Debug" or "Release". + */ +$wgHipHopBuildType = 'Debug'; /** - * Array of disabled article actions, e.g. view, edit, dublincore, delete, etc. + * Number of parallel processes to use during HipHop compilation, or "detect" + * to guess from system properties. */ -$wgDisabledActions = array(); +$wgHipHopCompilerProcs = 'detect'; + +/** + * Filesystem extensions directory. Defaults to $IP/../extensions. + * + * To compile extensions with HipHop, set $wgExtensionsDirectory correctly, + * and use code like: + * + * require( MWInit::extensionSetupPath( 'Extension/Extension.php' ) ); + * + * to include the extension setup file from LocalSettings.php. It is not + * necessary to set this variable unless you use MWInit::extensionSetupPath(). + */ +$wgExtensionsDirectory = false; + +/** + * A list of files that should be compiled into a HipHop build, in addition to + * those listed in $wgAutoloadClasses. Add to this array in an extension setup + * file in order to add files to the build. + * + * The files listed here must either be either absolute paths under $IP or + * under $wgExtensionsDirectory, or paths relative to the virtual source root + * "$IP/..", i.e. starting with "phase3" for core files, and "extensions" for + * extension files. + */ +$wgCompiledFiles = array(); + +/** @} */ # End of HipHop compilation } + + +/************************************************************************//** + * @name Miscellaneous + * @{ + */ + +/** Name of the external diff engine to use */ +$wgExternalDiffEngine = false; /** * Disable redirects to special pages and interwiki redirects, which use a 302 @@ -5296,6 +5549,8 @@ $wgUploadMaintenance = false; $wgEnableSelenium = false; $wgSeleniumTestConfigs = array(); $wgSeleniumConfigFile = null; +$wgDBtestuser = ''; //db user that has permission to create and drop the test databases only +$wgDBtestpassword = ''; /** * For really cool vim folding this needs to be at the end: diff --git a/includes/Defines.php b/includes/Defines.php index 64197d9c..ff7d7980 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -1,6 +1,11 @@ isSyndicated() is true - */ -$wgFeedClasses = array( - 'rss' => 'RSSFeed', - 'atom' => 'AtomFeed', -); - -/**@{ - * Maths constants - */ -define( 'MW_MATH_PNG', 0 ); -define( 'MW_MATH_SIMPLE', 1 ); -define( 'MW_MATH_HTML', 2 ); -define( 'MW_MATH_SOURCE', 3 ); -define( 'MW_MATH_MODERN', 4 ); -define( 'MW_MATH_MATHML', 5 ); -/**@}*/ - /**@{ * Cache type */ @@ -114,7 +98,7 @@ define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-st /**@{ * Media types. - * This defines constants for the value returned by Image::getMediaType() + * This defines constants for the value returned by File::getMediaType() */ define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); // unknown format define( 'MEDIATYPE_BITMAP', 'BITMAP' ); // some bitmap image or image source (like psd, etc). Can't scale up. @@ -254,4 +238,15 @@ define( 'APCOND_ISIP', 5 ); define( 'APCOND_IPINRANGE', 6 ); define( 'APCOND_AGE_FROM_EDIT', 7 ); define( 'APCOND_BLOCKED', 8 ); +define( 'APCOND_ISBOT', 9 ); /**@}*/ + +/** + * Protocol constants for wfExpandUrl() + */ +define( 'PROTO_HTTP', 'http://' ); +define( 'PROTO_HTTPS', 'https://' ); +define( 'PROTO_RELATIVE', '//' ); +define( 'PROTO_CURRENT', null ); +define( 'PROTO_CANONICAL', 1 ); +define( 'PROTO_INTERNAL', 2 ); diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php index cccb070a..80b7408c 100644 --- a/includes/DjVuImage.php +++ b/includes/DjVuImage.php @@ -72,7 +72,7 @@ class DjVuImage { function dump() { $file = fopen( $this->mFilename, 'rb' ); $header = fread( $file, 12 ); - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) ); echo "$chunk $chunkLength\n"; $this->dumpForm( $file, $chunkLength, 1 ); @@ -88,7 +88,7 @@ class DjVuImage { if( $chunkHeader == '' ) { break; } - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) ); echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n"; @@ -119,7 +119,7 @@ class DjVuImage { if( strlen( $header ) < 16 ) { wfDebug( __METHOD__ . ": too short file header\n" ); } else { - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) ); if( $magic != 'AT&T' ) { @@ -143,7 +143,7 @@ class DjVuImage { if( strlen( $header ) < 8 ) { return array( false, 0 ); } else { - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'a4chunk/Nlength', $header ) ); return array( $chunk, $length ); } @@ -202,7 +202,7 @@ class DjVuImage { return false; } - // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. + // @todo FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables. extract( unpack( 'nwidth/' . 'nheight/' . @@ -269,7 +269,7 @@ class DjVuImage { EOR; $txt = preg_replace_callback( $reg, array( $this, 'pageTextCallback' ), $txt ); $txt = "\n\n\n" . $txt . "\n\n"; - $xml = preg_replace( "//", "", $xml ); + $xml = preg_replace( "//", "", $xml, 1 ); $xml = $xml . $txt. '' ; } } diff --git a/includes/EditPage.php b/includes/EditPage.php index 3e85ad10..e6e7111d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -11,7 +11,7 @@ * interfaces. * * EditPage cares about two distinct titles: - * $wgTitle is the page that forms submit to, links point to, + * $this->mContextTitle is the page that forms submit to, links point to, * redirects go to, etc. $this->mTitle (as well as $mArticle) is the * page in the database that is actually being edited. These are * usually the same, but they are now allowed to be different. @@ -42,20 +42,31 @@ class EditPage { const AS_IMAGE_REDIRECT_ANON = 233; const AS_IMAGE_REDIRECT_LOGGED = 234; + /** + * @var Article + */ var $mArticle; + + /** + * @var Title + */ var $mTitle; + private $mContextTitle = null; var $action; var $isConflict = false; var $isCssJsSubpage = false; var $isCssSubpage = false; var $isJsSubpage = false; - var $deletedSinceEdit = false; + var $isWrongCaseCssJsPage = false; + var $isNew = false; // new page or new section + var $deletedSinceEdit; var $formtype; var $firsttime; var $lastDelete; var $mTokenOk = false; var $mTokenOkExceptSuffix = false; var $mTriedSave = false; + var $incompleteForm = false; var $tooBig = false; var $kblength = false; var $missingComment = false; @@ -64,7 +75,12 @@ class EditPage { var $autoSumm = ''; var $hookError = ''; #var $mPreviewTemplates; + + /** + * @var ParserOutput + */ var $mParserOutput; + var $mBaseRevision = false; var $mShowSummaryField = true; @@ -85,6 +101,7 @@ class EditPage { public $editFormTextBottom; public $editFormTextAfterContent; public $previewTextAfterContent; + public $mPreloadText; /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; @@ -94,7 +111,7 @@ class EditPage { /** * @todo document - * @param $article + * @param $article Article */ function __construct( $article ) { $this->mArticle =& $article; @@ -113,18 +130,47 @@ class EditPage { $this->mPreloadText = ""; } + /** + * @return Article + */ function getArticle() { return $this->mArticle; } + /** + * Set the context Title object + * + * @param $title Title object or null + */ + public function setContextTitle( $title ) { + $this->mContextTitle = $title; + } + + /** + * Get the context title object. + * If not set, $wgTitle will be returned. This behavior might changed in + * the future to return $this->mTitle instead. + * + * @return Title object + */ + public function getContextTitle() { + if ( is_null( $this->mContextTitle ) ) { + global $wgTitle; + return $wgTitle; + } else { + return $this->mContextTitle; + } + } /** * Fetch initial editing page content. + * + * @param $def_text string * @returns mixed string on success, $def_text for invalid sections * @private */ function getContent( $def_text = '' ) { - global $wgOut, $wgRequest, $wgParser, $wgContLang, $wgMessageCache; + global $wgOut, $wgRequest, $wgParser; wfProfileIn( __METHOD__ ); # Get variables from query string :P @@ -141,10 +187,10 @@ class EditPage { if ( !$this->mTitle->exists() ) { if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { # If this is a system message, get the default text. - list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) ); - $text = wfMsgGetKey( $message, false, $lang, false ); - if( wfEmptyMsg( $message, $text ) ) + $text = $this->mTitle->getDefaultMessageText(); + if( $text === false ) { $text = $this->getPreloadedText( $preload ); + } } else { # If requested, preload some text. $text = $this->getPreloadedText( $preload ); @@ -198,7 +244,7 @@ class EditPage { // was created, or we may simply have got bogus input. $this->editFormPageTop .= $wgOut->parse( '
    ' . wfMsgNoTrans( 'undo-norev' ) . '
    ' ); } - } else if ( $section != '' ) { + } elseif ( $section != '' ) { if ( $section == 'new' ) { $text = $this->getPreloadedText( $preload ); } else { @@ -212,7 +258,11 @@ class EditPage { return $text; } - /** Use this method before edit() to preload some text into the edit box */ + /** + * Use this method before edit() to preload some text into the edit box + * + * @param $text string + */ public function setPreloadedText( $text ) { $this->mPreloadText = $text; } @@ -253,15 +303,19 @@ class EditPage { return ''; } - /* + /** * Check if a page was deleted while the user was editing it, before submit. * Note that we rely on the logging table, which hasn't been always there, * but that doesn't matter, because this only applies to brand new * deletes. */ protected function wasDeletedSinceLastEdit() { - if ( $this->deletedSinceEdit ) - return true; + if ( $this->deletedSinceEdit !== null ) { + return $this->deletedSinceEdit; + } + + $this->deletedSinceEdit = false; + if ( $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { @@ -271,12 +325,15 @@ class EditPage { } } } + return $this->deletedSinceEdit; } /** * Checks whether the user entered a skin name in uppercase, * e.g. "User:Example/Monobook.css" instead of "monobook.css" + * + * @return bool */ protected function isWrongCaseCssJsPage() { if( $this->mTitle->isCssJsSubpage() ) { @@ -335,7 +392,7 @@ class EditPage { $this->preview = true; } - $wgOut->addModules( array( 'mediawiki.legacy.edit', 'mediawiki.action.edit' ) ); + $wgOut->addModules( array( 'mediawiki.action.edit' ) ); if ( $wgUser->getOption( 'uselivepreview', false ) ) { $wgOut->addModules( 'mediawiki.legacy.preview' ); @@ -345,6 +402,9 @@ class EditPage { $permErrors = $this->getEditPermissionErrors(); if ( $permErrors ) { + // Auto-block user's IP if the account was "hard" blocked + $wgUser->spreadAnyEditBlock(); + wfDebug( __METHOD__ . ": User can't edit\n" ); $content = $this->getContent( null ); $content = $content === '' ? null : $content; @@ -354,9 +414,9 @@ class EditPage { } else { if ( $this->save ) { $this->formtype = 'save'; - } else if ( $this->preview ) { + } elseif ( $this->preview ) { $this->formtype = 'preview'; - } else if ( $this->diff ) { + } elseif ( $this->diff ) { $this->formtype = 'diff'; } else { # First time through $this->firsttime = true; @@ -377,10 +437,11 @@ class EditPage { $this->isConflict = false; // css / js subpages of user pages get a special treatment - $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); - $this->isCssSubpage = $this->mTitle->isCssSubpage(); - $this->isJsSubpage = $this->mTitle->isJsSubpage(); + $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage(); + $this->isCssSubpage = $this->mTitle->isCssSubpage(); + $this->isJsSubpage = $this->mTitle->isJsSubpage(); $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage(); + $this->isNew = !$this->mTitle->exists() || $this->section == 'new'; # Show applicable editing introductions if ( $this->formtype == 'initial' || $this->firsttime ) @@ -392,16 +453,18 @@ class EditPage { # Optional notices on a per-namespace and per-page basis $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); - if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) { - $wgOut->addWikiText( wfMsgForContent( $editnotice_ns ) ); + $editnotice_ns_message = wfMessage( $editnotice_ns )->inContentLanguage(); + if ( $editnotice_ns_message->exists() ) { + $wgOut->addWikiText( $editnotice_ns_message->plain() ); } if ( MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) { $parts = explode( '/', $this->mTitle->getDBkey() ); $editnotice_base = $editnotice_ns; while ( count( $parts ) > 0 ) { $editnotice_base .= '-'.array_shift( $parts ); - if ( !wfEmptyMsg( $editnotice_base, wfMsgForContent( $editnotice_base ) ) ) { - $wgOut->addWikiText( wfMsgForContent( $editnotice_base ) ); + $editnotice_base_msg = wfMessage( $editnotice_base )->inContentLanguage(); + if ( $editnotice_base_msg->exists() ) { + $wgOut->addWikiText( $editnotice_base_msg->plain() ); } } } @@ -439,6 +502,9 @@ class EditPage { wfProfileOut( __METHOD__ ); } + /** + * @return array + */ protected function getEditPermissionErrors() { global $wgUser; $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ); @@ -532,7 +598,7 @@ class EditPage { /** * @todo document - * @param $request + * @param $request WebRequest */ function importFormData( &$request ) { global $wgLang, $wgUser; @@ -559,7 +625,7 @@ class EditPage { } # Truncate for whole multibyte characters. +5 bytes for ellipsis - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' ); + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); # Remove extra headings from summaries and new sections. $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary); @@ -569,7 +635,17 @@ class EditPage { $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' ); - if ( is_null( $this->edittime ) ) { + if ($this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null) { + // wpTextbox1 field is missing, possibly due to being "too big" + // according to some filter rules such as Suhosin's setting for + // suhosin.request.max_value_length (d'oh) + $this->incompleteForm = true; + } else { + // edittime should be one of our last fields; if it's missing, + // the submission probably broke somewhere in the middle. + $this->incompleteForm = is_null( $this->edittime ); + } + if ( $this->incompleteForm ) { # If the form is incomplete, force to preview. wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" ); wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); @@ -589,7 +665,7 @@ class EditPage { # The unmarked state will be assumed to be a save, # if the form seems otherwise complete. wfDebug( __METHOD__ . ": Passed token check.\n" ); - } else if ( $this->diff ) { + } elseif ( $this->diff ) { # Failed token check, but only requested "Show Changes". wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" ); } else { @@ -653,7 +729,7 @@ class EditPage { $this->bot = $request->getBool( 'bot', true ); $this->nosummary = $request->getBool( 'nosummary' ); - // FIXME: unused variable? + // @todo FIXME: Unused variable? $this->oldid = $request->getInt( 'oldid' ); $this->live = $request->getCheck( 'live' ); @@ -719,8 +795,8 @@ class EditPage { $ip = User::isIP( $username ); if ( !$user->isLoggedIn() && !$ip ) { # User does not exist $wgOut->wrapWikiMsg( "
    \n$1\n
    ", - array( 'userpage-userdoesnotexist', $username ) ); - } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked + array( 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ) ); + } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked LogEventsList::showLogExtract( $wgOut, 'block', @@ -766,8 +842,8 @@ class EditPage { $title = Title::newFromText( $this->editintro ); if ( $title instanceof Title && $title->exists() && $title->userCanRead() ) { global $wgOut; - $revision = Revision::newFromTitle( $title ); - $wgOut->addWikiTextTitleTidy( $revision->getText(), $this->mTitle ); + // Added using template syntax, to take 's into account. + $wgOut->addWikiTextTitleTidy( '{{:' . $title->getFullText() . '}}', $this->mTitle ); return true; } else { return false; @@ -779,32 +855,46 @@ class EditPage { /** * Attempt submission (no UI) - * @return one of the constants describing the result + * + * @param $result + * @param $bot bool + * + * @return Status object, possibly with a message, but always with one of the AS_* constants in $status->value, + * + * FIXME: This interface is TERRIBLE, but hard to get rid of due to various error display idiosyncrasies. There are + * also lots of cases where error metadata is set in the object and retrieved later instead of being returned, e.g. + * AS_CONTENT_TOO_BIG and AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some time. */ function internalAttemptSave( &$result, $bot = false ) { global $wgFilterCallback, $wgUser, $wgParser; global $wgMaxArticleSize; + + $status = Status::newGood(); wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ . '-checks' ); if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) { wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; + return $status; } # Check image redirect if ( $this->mTitle->getNamespace() == NS_FILE && Title::newFromRedirect( $this->textbox1 ) instanceof Title && !$wgUser->isAllowed( 'upload' ) ) { - $isAnon = $wgUser->isAnon(); + $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; + $status->setResult( false, $code ); wfProfileOut( __METHOD__ . '-checks' ); + wfProfileOut( __METHOD__ ); - return $isAnon ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED; + return $status; } # Check for spam @@ -818,276 +908,350 @@ class EditPage { $pdbk = $this->mTitle->getPrefixedDBkey(); $match = str_replace( "\n", '', $match ); wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" ); + $status->fatal( 'spamprotectionmatch', $match ); + $status->value = self::AS_SPAM_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_SPAM_ERROR; + return $status; } if ( $wgFilterCallback && $wgFilterCallback( $this->mTitle, $this->textbox1, $this->section, $this->hookError, $this->summary ) ) { # Error messages or other handling should be performed by the filter function + $status->setResult( false, self::AS_FILTERING ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_FILTERING; + return $status; } if ( !wfRunHooks( 'EditFilter', array( $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; + return $status; } elseif ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR_EXPECTED; + return $status; } if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) { + // Auto-block user's IP if the account was "hard" blocked + $wgUser->spreadAnyEditBlock(); # Check block state against master, thus 'false'. + $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_BLOCKED_PAGE_FOR_USER; + return $status; } $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { // Error will be displayed by showEditForm() $this->tooBig = true; + $status->setResult( false, self::AS_CONTENT_TOO_BIG ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_CONTENT_TOO_BIG; + return $status; } if ( !$wgUser->isAllowed( 'edit' ) ) { if ( $wgUser->isAnon() ) { + $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_READ_ONLY_PAGE_ANON; + return $status; } else { + $status->fatal( 'readonlytext' ); + $status->value = self::AS_READ_ONLY_PAGE_LOGGED; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_READ_ONLY_PAGE_LOGGED; + return $status; } } if ( wfReadOnly() ) { + $status->fatal( 'readonlytext' ); + $status->value = self::AS_READ_ONLY_PAGE; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_READ_ONLY_PAGE; + return $status; } if ( $wgUser->pingLimiter() ) { + $status->fatal( 'actionthrottledtext' ); + $status->value = self::AS_RATE_LIMITED; wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_RATE_LIMITED; + return $status; } # If the article has been deleted while editing, don't save it without # confirmation if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) { + $status->setResult( false, self::AS_ARTICLE_WAS_DELETED ); wfProfileOut( __METHOD__ . '-checks' ); wfProfileOut( __METHOD__ ); - return self::AS_ARTICLE_WAS_DELETED; + return $status; } wfProfileOut( __METHOD__ . '-checks' ); # If article is new, insert it. $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE ); - if ( 0 == $aid ) { + $new = ( $aid == 0 ); + + if ( $new ) { // Late check for create permission, just in case *PARANOIA* if ( !$this->mTitle->userCan( 'create' ) ) { + $status->fatal( 'nocreatetext' ); + $status->value = self::AS_NO_CREATE_PERMISSION; wfDebug( __METHOD__ . ": no create permission\n" ); wfProfileOut( __METHOD__ ); - return self::AS_NO_CREATE_PERMISSION; + return $status; } # Don't save a new article if it's blank. if ( $this->textbox1 == '' ) { + $status->setResult( false, self::AS_BLANK_ARTICLE ); wfProfileOut( __METHOD__ ); - return self::AS_BLANK_ARTICLE; + return $status; } // Run post-section-merge edit filter if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) { # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; + return $status; } elseif ( $this->hookError != '' ) { # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR_EXPECTED; + return $status; } # Handle the user preference to force summaries here. Check if it's not a redirect. if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; wfProfileOut( __METHOD__ ); - return self::AS_SUMMARY_NEEDED; + return $status; } } - $isComment = ( $this->section == 'new' ); + $text = $this->textbox1; + if ( $this->section == 'new' && $this->summary != '' ) { + $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text; + } - $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, - $this->minoredit, $this->watchthis, false, $isComment, $bot ); + $status->value = self::AS_SUCCESS_NEW_ARTICLE; - wfProfileOut( __METHOD__ ); - return self::AS_SUCCESS_NEW_ARTICLE; - } + } else { - # Article exists. Check for edit conflict. + # Article exists. Check for edit conflict. - $this->mArticle->clear(); # Force reload of dates, etc. - $this->mArticle->forUpdate( true ); # Lock the article + $this->mArticle->clear(); # Force reload of dates, etc. - wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); + wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" ); - if ( $this->mArticle->getTimestamp() != $this->edittime ) { - $this->isConflict = true; - if ( $this->section == 'new' ) { - if ( $this->mArticle->getUserText() == $wgUser->getName() && - $this->mArticle->getComment() == $this->summary ) { - // Probably a duplicate submission of a new comment. - // This can happen when squid resends a request after - // a timeout but the first one actually went through. - wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); - } else { - // New comment; suppress conflict. - $this->isConflict = false; - wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); + if ( $this->mArticle->getTimestamp() != $this->edittime ) { + $this->isConflict = true; + if ( $this->section == 'new' ) { + if ( $this->mArticle->getUserText() == $wgUser->getName() && + $this->mArticle->getComment() == $this->summary ) { + // Probably a duplicate submission of a new comment. + // This can happen when squid resends a request after + // a timeout but the first one actually went through. + wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" ); + } else { + // New comment; suppress conflict. + $this->isConflict = false; + wfDebug( __METHOD__ .": conflict suppressed; new section\n" ); + } } } - } - $userid = $wgUser->getId(); - - # Suppress edit conflict with self, except for section edits where merging is required. - if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) { - wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; - } + $userid = $wgUser->getId(); - if ( $this->isConflict ) { - wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" . - $this->mArticle->getTimestamp() . "')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); - } else { - wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); - } - if ( is_null( $text ) ) { - wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); - $this->isConflict = true; - $text = $this->textbox1; // do not try to merge here! - } else if ( $this->isConflict ) { - # Attempt merge - if ( $this->mergeChangesInto( $text ) ) { - // Successful merge! Maybe we should tell the user the good news? + # Suppress edit conflict with self, except for section edits where merging is required. + if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) { + wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" ); $this->isConflict = false; - wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); - } else { - $this->section = ''; - $this->textbox1 = $text; - wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); } - } - if ( $this->isConflict ) { - wfProfileOut( __METHOD__ ); - return self::AS_CONFLICT_DETECTED; - } - - $oldtext = $this->mArticle->getContent(); - - // Run post-section-merge edit filter - if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { - # Error messages etc. could be handled within the hook... - wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR; - } elseif ( $this->hookError != '' ) { - # ...or the hook could be expecting us to produce an error - wfProfileOut( __METHOD__ ); - return self::AS_HOOK_ERROR_EXPECTED; - } + if ( $this->isConflict ) { + wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" . + $this->mArticle->getTimestamp() . "')\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); + } else { + wfDebug( __METHOD__ . ": getting section '$this->section'\n" ); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); + } + if ( is_null( $text ) ) { + wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" ); + $this->isConflict = true; + $text = $this->textbox1; // do not try to merge here! + } elseif ( $this->isConflict ) { + # Attempt merge + if ( $this->mergeChangesInto( $text ) ) { + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = $text; + wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" ); + } + } - # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text ) - && !Title::newFromRedirect( $text ) ) # check if it's not a redirect - { - if ( md5( $this->summary ) == $this->autoSumm ) { - $this->missingSummary = true; + if ( $this->isConflict ) { + $status->setResult( false, self::AS_CONFLICT_DETECTED ); wfProfileOut( __METHOD__ ); - return self::AS_SUMMARY_NEEDED; + return $status; } - } - # And a similar thing for new sections - if ( $this->section == 'new' && !$this->allowBlankSummary ) { - if ( trim( $this->summary ) == '' ) { - $this->missingSummary = true; + $oldtext = $this->mArticle->getContent(); + + // Run post-section-merge edit filter + if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) { + # Error messages etc. could be handled within the hook... + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR; + wfProfileOut( __METHOD__ ); + return $status; + } elseif ( $this->hookError != '' ) { + # ...or the hook could be expecting us to produce an error + $status->fatal( 'hookaborted' ); + $status->value = self::AS_HOOK_ERROR_EXPECTED; wfProfileOut( __METHOD__ ); - return self::AS_SUMMARY_NEEDED; + return $status; } - } - # All's well - wfProfileIn( __METHOD__ . '-sectionanchor' ); - $sectionanchor = ''; - if ( $this->section == 'new' ) { - if ( $this->textbox1 == '' ) { - $this->missingComment = true; - wfProfileOut( __METHOD__ . '-sectionanchor' ); - wfProfileOut( __METHOD__ ); - return self::AS_TEXTBOX_EMPTY; + # Handle the user preference to force summaries here, but not for null edits + if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text ) + && !Title::newFromRedirect( $text ) ) # check if it's not a redirect + { + if ( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; + } } - if ( $this->summary != '' ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); - # This is a new section, so create a link to the new section - # in the revision summary. - $cleanSummary = $wgParser->stripSectionName( $this->summary ); - $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + + # And a similar thing for new sections + if ( $this->section == 'new' && !$this->allowBlankSummary ) { + if ( trim( $this->summary ) == '' ) { + $this->missingSummary = true; + $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh + $status->value = self::AS_SUMMARY_NEEDED; + wfProfileOut( __METHOD__ ); + return $status; + } } - } elseif ( $this->section != '' ) { - # Try to get a section anchor from the section source, redirect to edited section if header found - # XXX: might be better to integrate this into Article::replaceSection - # for duplicate heading checking and maybe parsing - $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); - # we can't deal with anchors, includes, html etc in the header for now, - # headline would need to be parsed to improve this - if ( $hasmatch and strlen( $matches[2] ) > 0 ) { - $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); + + # All's well + wfProfileIn( __METHOD__ . '-sectionanchor' ); + $sectionanchor = ''; + if ( $this->section == 'new' ) { + if ( $this->textbox1 == '' ) { + $this->missingComment = true; + $status->fatal( 'missingcommenttext' ); + $status->value = self::AS_TEXTBOX_EMPTY; + wfProfileOut( __METHOD__ . '-sectionanchor' ); + wfProfileOut( __METHOD__ ); + return $status; + } + if ( $this->summary != '' ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary ); + # This is a new section, so create a link to the new section + # in the revision summary. + $cleanSummary = $wgParser->stripSectionName( $this->summary ); + $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary ); + } + } elseif ( $this->section != '' ) { + # Try to get a section anchor from the section source, redirect to edited section if header found + # XXX: might be better to integrate this into Article::replaceSection + # for duplicate heading checking and maybe parsing + $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches ); + # we can't deal with anchors, includes, html etc in the header for now, + # headline would need to be parsed to improve this + if ( $hasmatch && strlen( $matches[2] ) > 0 ) { + $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] ); + } } - } - wfProfileOut( __METHOD__ . '-sectionanchor' ); + $result['sectionanchor'] = $sectionanchor; + wfProfileOut( __METHOD__ . '-sectionanchor' ); + + // Save errors may fall down to the edit form, but we've now + // merged the section into full text. Clear the section field + // so that later submission of conflict forms won't try to + // replace that into a duplicated mess. + $this->textbox1 = $text; + $this->section = ''; - // Save errors may fall down to the edit form, but we've now - // merged the section into full text. Clear the section field - // so that later submission of conflict forms won't try to - // replace that into a duplicated mess. - $this->textbox1 = $text; - $this->section = ''; + $status->value = self::AS_SUCCESS_UPDATE; + } // Check for length errors again now that the section is merged in $this->kblength = (int)( strlen( $text ) / 1024 ); if ( $this->kblength > $wgMaxArticleSize ) { $this->tooBig = true; + $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED ); wfProfileOut( __METHOD__ ); - return self::AS_MAX_ARTICLE_SIZE_EXCEEDED; + return $status; } - # update the article here - if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, - $this->watchthis, $bot, $sectionanchor ) ) - { + $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY | + ( $new ? EDIT_NEW : EDIT_UPDATE ) | + ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) | + ( $bot ? EDIT_FORCE_BOT : 0 ); + + $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags ); + + if ( $doEditStatus->isOK() ) { + $result['redirect'] = Title::newFromRedirect( $text ) !== null; + $this->commitWatch(); wfProfileOut( __METHOD__ ); - return self::AS_SUCCESS_UPDATE; + return $status; } else { $this->isConflict = true; + $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares + wfProfileOut( __METHOD__ ); + return $doEditStatus; + } + } + + /** + * Commit the change of watch status + */ + protected function commitWatch() { + global $wgUser; + if ( $this->watchthis xor $this->mTitle->userIsWatching() ) { + $dbw = wfGetDB( DB_MASTER ); + $dbw->begin(); + if ( $this->watchthis ) { + WatchAction::doWatch( $this->mTitle, $wgUser ); + } else { + WatchAction::doUnwatch( $this->mTitle, $wgUser ); + } + $dbw->commit(); } - wfProfileOut( __METHOD__ ); - return self::AS_END; } /** * Check if no edits were made by other users since * the time a user started editing the page. Limit to * 50 revisions for the sake of performance. + * + * @param $id int + * @param $edittime string + * + * @return bool */ protected function userWasLastToEdit( $id, $edittime ) { if( !$id ) return false; @@ -1110,7 +1274,10 @@ class EditPage { /** * Check given input text against $wgSpamRegex, and return the text of the first match. - * @return mixed -- matching string or false + * + * @param $text string + * + * @return string|false matching string or false */ public static function matchSpamRegex( $text ) { global $wgSpamRegex; @@ -1121,7 +1288,10 @@ class EditPage { /** * Check given input text against $wgSpamRegex, and return the text of the first match. - * @return mixed -- matching string or false + * + * @parma $text string + * + * @return string|false matching string or false */ public static function matchSummarySpamRegex( $text ) { global $wgSummarySpamRegex; @@ -1129,6 +1299,11 @@ class EditPage { return self::matchSpamRegexInternal( $text, $regexes ); } + /** + * @param $text string + * @param $regexes array + * @return bool|string + */ protected static function matchSpamRegexInternal( $text, $regexes ) { foreach( $regexes as $regex ) { $matches = array(); @@ -1142,7 +1317,7 @@ class EditPage { /** * Initialise form fields in the object * Called on the first invocation, e.g. when a user clicks an edit link - * @returns bool -- if the requested section is valid + * @return bool -- if the requested section is valid */ function initialiseForm() { global $wgUser; @@ -1160,30 +1335,34 @@ class EditPage { # Already watched $this->watchthis = true; } - if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true; - if ( $this->textbox1 === false ) return false; + if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) { + $this->minoredit = true; + } + if ( $this->textbox1 === false ) { + return false; + } wfProxyCheck(); return true; } function setHeaders() { - global $wgOut, $wgTitle; + global $wgOut; $wgOut->setRobotPolicy( 'noindex,nofollow' ); if ( $this->formtype == 'preview' ) { $wgOut->setPageTitleActionText( wfMsg( 'preview' ) ); } if ( $this->isConflict ) { - $wgOut->setPageTitle( wfMsg( 'editconflict', $wgTitle->getPrefixedText() ) ); + $wgOut->setPageTitle( wfMsg( 'editconflict', $this->getContextTitle()->getPrefixedText() ) ); } elseif ( $this->section != '' ) { $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection'; - $wgOut->setPageTitle( wfMsg( $msg, $wgTitle->getPrefixedText() ) ); + $wgOut->setPageTitle( wfMsg( $msg, $this->getContextTitle()->getPrefixedText() ) ); } else { # Use the title defined by DISPLAYTITLE magic word when present if ( isset( $this->mParserOutput ) && ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) { $title = $dt; } else { - $title = $wgTitle->getPrefixedText(); + $title = $this->getContextTitle()->getPrefixedText(); } $wgOut->setPageTitle( wfMsg( 'editing', $title ) ); } @@ -1191,24 +1370,14 @@ class EditPage { /** * Send the edit form and related headers to $wgOut - * @param $formCallback Optional callable that takes an OutputPage - * parameter; will be called during form output - * near the top, for captchas and the like. + * @param $formCallback Callback that takes an OutputPage parameter; will be called + * during form output near the top, for captchas and the like. */ function showEditForm( $formCallback = null ) { - global $wgOut, $wgUser, $wgTitle; - - # If $wgTitle is null, that means we're in API mode. - # Some hook probably called this function without checking - # for is_null($wgTitle) first. Bail out right here so we don't - # do lots of work just to discard it right after. - if ( is_null( $wgTitle ) ) - return; + global $wgOut, $wgUser; wfProfileIn( __METHOD__ ); - $sk = $wgUser->getSkin(); - #need to parse the preview early so that we know which templates are used, #otherwise users with "show preview after edit box" will get a blank list #we parse this near the beginning so that setHeaders can do the title @@ -1230,7 +1399,7 @@ class EditPage { return; } - $action = htmlspecialchars( $this->getActionURL( $wgTitle ) ); + $action = htmlspecialchars( $this->getActionURL( $this->getContextTitle() ) ); if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) { # prepare toolbar for edit buttons @@ -1249,10 +1418,10 @@ class EditPage { $wgOut->addHTML( $this->editFormTextTop ); $templates = $this->getTemplates(); - $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != ''); + $formattedtemplates = Linker::formatTemplates( $templates, $this->preview, $this->section != ''); $hiddencats = $this->mArticle->getHiddenCategories(); - $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats ); + $formattedhiddencats = Linker::formatHiddenCategories( $hiddencats ); if ( $this->wasDeletedSinceLastEdit() && 'save' != $this->formtype ) { $wgOut->wrapWikiMsg( @@ -1265,7 +1434,6 @@ class EditPage { // @todo move this to a cleaner conditional instead of blanking a variable } $wgOut->addHTML( << HTML ); @@ -1280,11 +1448,19 @@ HTML $this->showFormBeforeText(); if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) { + $username = $this->lastDelete->user_name; + $comment = $this->lastDelete->log_comment; + + // It is better to not parse the comment at all than to have templates expanded in the middle + // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used? + $key = $comment === '' + ? 'confirmrecreate-noreason' + : 'confirmrecreate'; $wgOut->addHTML( '
    ' . - $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . + wfMsgExt( $key, 'parseinline', $username, "$comment" ) . Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, - array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) + array( 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) ) . '
    ' ); @@ -1312,6 +1488,8 @@ HTML $wgOut->addHTML( $this->editFormTextBeforeContent ); + $wgOut->addHTML( $toolbar ); + if ( $this->isConflict ) { // In an edit conflict bypass the overrideable content form method // and fallback to the raw wpTextbox1 since editconflicts can't be @@ -1359,7 +1537,7 @@ HTML } protected function showHeader() { - global $wgOut, $wgUser, $wgTitle, $wgMaxArticleSize, $wgLang; + global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang; if ( $this->isConflict ) { $wgOut->wrapWikiMsg( "
    \n$1\n
    ", 'explainconflict' ); $this->edittime = $this->mArticle->getTimestamp(); @@ -1410,7 +1588,7 @@ HTML if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); - } else if ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { + } elseif ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-view' ); } @@ -1433,7 +1611,7 @@ HTML if ( $this->isCssJsSubpage ) { # Check the skin exists if ( $this->isWrongCaseCssJsPage ) { - $wgOut->wrapWikiMsg( "
    \n$1\n
    ", array( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() ) ); + $wgOut->wrapWikiMsg( "
    \n$1\n
    ", array( 'userinvalidcssjstitle', $this->getContextTitle()->getSkinFromCssJsSubpage() ) ); } if ( $this->formtype !== 'preview' ) { if ( $this->isCssSubpage ) @@ -1485,9 +1663,7 @@ HTML $wgOut->wrapWikiMsg( "
    \n$1\n
    ", array( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) ) ); } else { - $msg = 'longpage-hint'; - $text = wfMsg( $msg ); - if( !wfEmptyMsg( $msg, $text ) && $text !== '-' ) { + if( !wfMessage('longpage-hint')->isDisabled() ) { $wgOut->wrapWikiMsg( "
    \n$1\n
    ", array( 'longpage-hint', $wgLang->formatSize( strlen( $this->textbox1 ) ), strlen( $this->textbox1 ) ) ); @@ -1502,15 +1678,14 @@ HTML * inferred by the id given to the input. You can remove them both by * passing array( 'id' => false ) to $userInputAttrs. * - * @param $summary The value of the summary input - * @param $labelText The html to place inside the label - * @param $inputAttrs An array of attrs to use on the input - * @param $spanLabelAttrs An array of attrs to use on the span inside the label + * @param $summary string The value of the summary input + * @param $labelText string The html to place inside the label + * @param $inputAttrs array of attrs to use on the input + * @param $spanLabelAttrs array of attrs to use on the span inside the label * * @return array An array in the format array( $label, $input ) */ function getSummaryInput($summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null) { - global $wgUser; //Note: the maxlength is overriden in JS to 250 and to make it use UTF-8 bytes, not characters. $inputAttrs = ( is_array($inputAttrs) ? $inputAttrs : array() ) + array( 'id' => 'wpSummary', @@ -1518,7 +1693,7 @@ HTML 'tabindex' => '1', 'size' => 60, 'spellcheck' => 'true', - ) + $wgUser->getSkin()->tooltipAndAccessKeyAttribs( 'summary' ); + ) + Linker::tooltipAndAccesskeyAttribs( 'summary' ); $spanLabelAttrs = ( is_array($spanLabelAttrs) ? $spanLabelAttrs : array() ) + array( 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary', @@ -1548,11 +1723,13 @@ HTML # Add a class if 'missingsummary' is triggered to allow styling of the summary line $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; if ( $isSubjectPreview ) { - if ( $this->nosummary ) + if ( $this->nosummary ) { return; + } } else { - if ( !$this->mShowSummaryField ) + if ( !$this->mShowSummaryField ) { return; + } } $summary = $wgContLang->recodeForEdit( $summary ); $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' ); @@ -1571,15 +1748,14 @@ HTML if ( !$summary || ( !$this->preview && !$this->diff ) ) return ""; - global $wgParser, $wgUser; - $sk = $wgUser->getSkin(); + global $wgParser; if ( $isSubjectPreview ) $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) ); $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview'; - $summary = wfMsgExt( $message, 'parseinline' ) . $sk->commentBlock( $summary, $this->mTitle, $isSubjectPreview ); + $summary = wfMsgExt( $message, 'parseinline' ) . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview ); return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary ); } @@ -1645,6 +1821,10 @@ HTML # Then it must be protected based on static groups (regular) $classes[] = 'mw-textarea-protected'; } + # Is the title cascade-protected? + if ( $this->mTitle->isCascadeProtected() ) { + $classes[] = 'mw-textarea-cprotected'; + } } $attribs = array( 'tabindex' => 1 ); if ( is_array($customAttribs) ) @@ -1662,7 +1842,7 @@ HTML } protected function showTextbox2() { - $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) ); + $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) ); } protected function showTextbox( $content, $name, $customAttribs = array() ) { @@ -1685,6 +1865,10 @@ HTML 'style' => '' // avoid php notices when appending preferences (appending allows customAttribs['style'] to still work ); + $pageLang = $this->mTitle->getPageLanguage(); + $attribs['lang'] = $pageLang->getCode(); + $attribs['dir'] = $pageLang->getDir(); + $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) ); } @@ -1743,20 +1927,19 @@ HTML protected function showTosSummary() { $msg = 'editpage-tos-summary'; wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) ); - $text = wfMsg( $msg ); - if( !wfEmptyMsg( $msg, $text ) && $text !== '-' ) { + if( !wfMessage( $msg )->isDisabled() ) { global $wgOut; $wgOut->addHTML( '
    ' ); - $wgOut->addWikiMsgArray( $msg, array() ); + $wgOut->addWikiMsg( $msg ); $wgOut->addHTML( '
    ' ); } } protected function showEditTools() { global $wgOut; - $wgOut->addHTML( '
    ' ); - $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); - $wgOut->addHTML( '
    ' ); + $wgOut->addHTML( '
    ' . + wfMessage( 'edittools' )->inContentLanguage()->parse() . + '
    ' ); } protected function getCopywarn() { @@ -1772,11 +1955,12 @@ HTML // Allow for site and per-namespace customization of contribution/copyright notice. wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) ); - return "
    \n" . call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n
    "; + return "
    \n" . + call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n
    "; } protected function showStandardInputs( &$tabindex = 2 ) { - global $wgOut, $wgUser; + global $wgOut; $wgOut->addHTML( "
    \n" ); if ( $this->section != 'new' ) { @@ -1784,23 +1968,25 @@ HTML $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) ); } - $checkboxes = $this->getCheckboxes( $tabindex, $wgUser->getSkin(), + $checkboxes = $this->getCheckboxes( $tabindex, array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) ); $wgOut->addHTML( "
    " . implode( $checkboxes, "\n" ) . "
    \n" ); $wgOut->addHTML( "
    \n" ); $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" ); $cancel = $this->getCancelLink(); - $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + if ( $cancel !== '' ) { + $cancel .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + } $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) ); $edithelp = ''. htmlspecialchars( wfMsg( 'edithelp' ) ).' '. htmlspecialchars( wfMsg( 'newwindow' ) ); - $wgOut->addHTML( " {$cancel}{$separator}{$edithelp}\n" ); + $wgOut->addHTML( " {$cancel}{$edithelp}\n" ); $wgOut->addHTML( "
    \n
    \n" ); } - /* + /** * Show an edit conflict. textbox1 is already shown in showEditForm(). * If you want to use another entry point to this function, be careful. */ @@ -1825,20 +2011,20 @@ HTML $data = $dbr->selectRow( array( 'logging', 'user' ), array( 'log_type', - 'log_action', - 'log_timestamp', - 'log_user', - 'log_namespace', - 'log_title', - 'log_comment', - 'log_params', - 'log_deleted', - 'user_name' ), + 'log_action', + 'log_timestamp', + 'log_user', + 'log_namespace', + 'log_title', + 'log_comment', + 'log_params', + 'log_deleted', + 'user_name' ), array( 'log_namespace' => $this->mTitle->getNamespace(), - 'log_title' => $this->mTitle->getDBkey(), - 'log_type' => 'delete', - 'log_action' => 'delete', - 'user_id=log_user' ), + 'log_title' => $this->mTitle->getDBkey(), + 'log_type' => 'delete', + 'log_action' => 'delete', + 'user_id=log_user' ), __METHOD__, array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); @@ -1857,7 +2043,7 @@ HTML * @return string */ function getPreviewText() { - global $wgOut, $wgUser, $wgParser, $wgMessageCache; + global $wgOut, $wgUser, $wgParser; wfProfileIn( __METHOD__ ); @@ -1867,6 +2053,8 @@ HTML } else { $note = wfMsg( 'session_fail_preview' ); } + } elseif ( $this->incompleteForm ) { + $note = wfMsg( 'edit_form_incomplete' ); } else { $note = wfMsg( 'previewnote' ); } @@ -1880,14 +2068,20 @@ HTML if ( $wgRawHtml && !$this->mTokenOk ) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. - $parsedNote = $wgOut->parse( "
    " . - wfMsg( 'session_fail_preview_html' ) . "
    " ); + $parsedNote = ''; + if ( $this->textbox1 !== '' ) { + // Do not put big scary notice, if previewing the empty + // string, which happens when you initially edit + // a category page, due to automatic preview-on-open. + $parsedNote = $wgOut->parse( "
    " . + wfMsg( 'session_fail_preview_html' ) . "
    " ); + } wfProfileOut( __METHOD__ ); return $parsedNote; } # don't parse user css/js, show message about preview - # XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago? + # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago? if ( $this->isCssJsSubpage || $this->mTitle->isCssOrJsPage() ) { $level = 'user'; @@ -1926,13 +2120,6 @@ HTML wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) ); - // Parse mediawiki messages with correct target language - if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - list( /* $unused */, $lang ) = $wgMessageCache->figureMessage( $this->mTitle->getText() ); - $obj = wfGetLangObj( $lang ); - $parserOptions->setTargetLanguage( $obj ); - } - $parserOptions->setTidy( true ); $parserOptions->enableLimitReport(); $parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ), @@ -1958,14 +2145,24 @@ HTML '

    ' . htmlspecialchars( wfMsg( 'preview' ) ) . "

    " . $wgOut->parse( $note ) . $conflict . "\n"; + $pageLang = $this->mTitle->getPageLanguage(); + $attribs = array( 'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(), + 'class' => 'mw-content-'.$pageLang->getDir() ); + $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML ); + wfProfileOut( __METHOD__ ); return $previewhead . $previewHTML . $this->previewTextAfterContent; } + /** + * @return Array + */ function getTemplates() { if ( $this->preview || $this->section != '' ) { $templates = array(); - if ( !isset( $this->mParserOutput ) ) return $templates; + if ( !isset( $this->mParserOutput ) ) { + return $templates; + } foreach( $this->mParserOutput->getTemplates() as $ns => $template) { foreach( array_keys( $template ) as $dbk ) { $templates[] = Title::makeTitle($ns, $dbk); @@ -2005,24 +2202,22 @@ HTML * Produce the stock "please login to edit pages" page */ function userNotLoggedInPage() { - global $wgUser, $wgOut, $wgTitle; - $skin = $wgUser->getSkin(); + global $wgOut; $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); - $loginLink = $skin->link( + $loginLink = Linker::linkKnown( $loginTitle, wfMsgHtml( 'loginreqlink' ), array(), - array( 'returnto' => $wgTitle->getPrefixedText() ), - array( 'known', 'noclasses' ) + array( 'returnto' => $this->getContextTitle()->getPrefixedText() ) ); $wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) ); $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setArticleRelated( false ); - $wgOut->addHTML( wfMsgWikiHtml( 'whitelistedittext', $loginLink ) ); - $wgOut->returnToMain( false, $wgTitle ); + $wgOut->addHTML( wfMessage( 'whitelistedittext' )->rawParams( $loginLink )->parse() ); + $wgOut->returnToMain( false, $this->getContextTitle() ); } /** @@ -2047,7 +2242,7 @@ HTML * Produce the stock "your edit contains spam" page * * @param $match Text which triggered one or more filters - * @deprecated Use method spamPageWithContent() instead + * @deprecated since 1.17 Use method spamPageWithContent() instead */ static function spamPage( $match = false ) { global $wgOut, $wgTitle; @@ -2072,7 +2267,7 @@ HTML * @param $match Text which triggered one or more filters */ public function spamPageWithContent( $match = false ) { - global $wgOut, $wgTitle; + global $wgOut; $this->textbox2 = $this->textbox1; $wgOut->setPageTitle( wfMsg( 'spamprotectiontitle' ) ); @@ -2094,13 +2289,17 @@ HTML $wgOut->wrapWikiMsg( '

    $1

    ', "yourtext" ); $this->showTextbox2(); - $wgOut->addReturnTo( $wgTitle, array( 'action' => 'edit' ) ); + $wgOut->addReturnTo( $this->getContextTitle(), array( 'action' => 'edit' ) ); } /** * @private * @todo document + * + * @parma $editText string + * + * @return bool */ function mergeChangesInto( &$editText ){ wfProfileIn( __METHOD__ ); @@ -2156,14 +2355,6 @@ HTML return true; } - /** - * @deprecated use $wgParser->stripSectionName() - */ - function pseudoParseSectionAnchor( $text ) { - global $wgParser; - return $wgParser->stripSectionName( $text ); - } - /** * Format an anchor fragment as it would appear for a given section name * @param $text String @@ -2189,16 +2380,19 @@ HTML $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); /** - - * toolarray an array of arrays which each include the filename of - * the button image (without path), the opening tag, the closing tag, - * and optionally a sample text that is inserted between the two when no - * selection is highlighted. - * The tip text is shown when the user moves the mouse over the button. + * $toolarray is an array of arrays each of which includes the + * filename of the button image (without path), the opening + * tag, the closing tag, optionally a sample text that is + * inserted between the two when no selection is highlighted + * and an option to select which switches the automatic + * selection of inserted text (default is true, see + * mw-editbutton-image). The tip text is shown when the user + * moves the mouse over the button. * - * Already here are accesskeys (key), which are not used yet until someone - * can figure out a way to make them work in IE. However, we should make - * sure these keys are not defined on the edit page. + * Also here: accesskeys (key), which are not used yet until + * someone can figure out a way to make them work in + * IE. However, we should make sure these keys are not defined + * on the edit page. */ $toolarray = array( array( @@ -2253,7 +2447,8 @@ HTML 'close' => ']]', 'sample' => wfMsg( 'image_sample' ), 'tip' => wfMsg( 'image_tip' ), - 'key' => 'D' + 'key' => 'D', + 'select' => true ) : false, $imagesAvailable ? array( 'image' => $wgLang->getImageFile( 'button-media' ), @@ -2301,7 +2496,6 @@ HTML 'key' => 'R' ) ); - $toolbar = "
    \n"; $script = ''; foreach ( $toolarray as $tool ) { @@ -2309,6 +2503,10 @@ HTML continue; } + if( !isset( $tool['select'] ) ) { + $tool['select'] = true; + } + $params = array( $image = $wgStylePath . '/common/images/' . $tool['image'], // Note that we use the tip both for the ALT tag and the TITLE tag of the image. @@ -2322,16 +2520,11 @@ HTML $cssId = $tool['id'], ); - $paramList = implode( ',', - array_map( array( 'Xml', 'encodeJsVar' ), $params ) ); - $script .= "addButton($paramList);\n"; + $script .= Xml::encodeJsCall( 'mw.toolbar.addButton', $params ); } - - $wgOut->addScript( Html::inlineScript( - "if ( window.mediaWiki ) { $script }" - ) ); - - $toolbar .= "\n
    "; + $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) ); + + $toolbar = '
    '; wfRunHooks( 'EditPageBeforeEditToolbar', array( &$toolbar ) ); @@ -2343,30 +2536,32 @@ HTML * minor and watch * * @param $tabindex Current tabindex - * @param $skin Skin object * @param $checked Array of checkbox => bool, where bool indicates the checked * status of the checkbox * * @return array */ - public function getCheckboxes( &$tabindex, $skin, $checked ) { + public function getCheckboxes( &$tabindex, $checked ) { global $wgUser; $checkboxes = array(); - $checkboxes['minor'] = ''; - $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) ); - if ( $wgUser->isAllowed( 'minoredit' ) ) { - $attribs = array( - 'tabindex' => ++$tabindex, - 'accesskey' => wfMsg( 'accesskey-minoredit' ), - 'id' => 'wpMinoredit', - ); - $checkboxes['minor'] = - Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . - " "; + // don't show the minor edit checkbox if it's a new page or section + if ( !$this->isNew ) { + $checkboxes['minor'] = ''; + $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) ); + if ( $wgUser->isAllowed( 'minoredit' ) ) { + $attribs = array( + 'tabindex' => ++$tabindex, + 'accesskey' => wfMsg( 'accesskey-minoredit' ), + 'id' => 'wpMinoredit', + ); + $checkboxes['minor'] = + Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) . + " "; + } } $watchLabel = wfMsgExt( 'watchthis', array( 'parseinline' ) ); @@ -2380,7 +2575,7 @@ HTML $checkboxes['watch'] = Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) . " "; } wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) ); @@ -2467,21 +2662,20 @@ HTML echo $s; } - + /** + * @return string + */ public function getCancelLink() { - global $wgUser, $wgTitle; - $cancelParams = array(); if ( !$this->isConflict && $this->mArticle->getOldID() > 0 ) { $cancelParams['oldid'] = $this->mArticle->getOldID(); } - return $wgUser->getSkin()->link( - $wgTitle, + return Linker::linkKnown( + $this->getContextTitle(), wfMsgExt( 'cancel', array( 'parseinline' ) ), array( 'id' => 'mw-editform-cancel' ), - $cancelParams, - array( 'known', 'noclasses' ) + $cancelParams ); } @@ -2531,6 +2725,11 @@ HTML : $text; } + /** + * @param $request WebRequest + * @param $text string + * @return string + */ function safeUnicodeText( $request, $text ) { $text = rtrim( $text ); return $request->getBool( 'safemode' ) @@ -2575,7 +2774,7 @@ HTML $result = ""; $working = 0; for( $i = 0; $i < strlen( $invalue ); $i++ ) { - $bytevalue = ord( $invalue{$i} ); + $bytevalue = ord( $invalue[$i] ); if ( $bytevalue <= 0x7F ) { //0xxx xxxx $result .= chr( $bytevalue ); $bytesleft = 0; @@ -2612,13 +2811,13 @@ HTML function unmakesafe( $invalue ) { $result = ""; for( $i = 0; $i < strlen( $invalue ); $i++ ) { - if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue{$i+3} != '0' ) ) { + if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i+3] != '0' ) ) { $i += 3; $hexstring = ""; do { - $hexstring .= $invalue{$i}; + $hexstring .= $invalue[$i]; $i++; - } while( ctype_xdigit( $invalue{$i} ) && ( $i < strlen( $invalue ) ) ); + } while( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) ); // Do some sanity checks. These aren't needed for reversability, // but should help keep the breakage down if the editor @@ -2648,21 +2847,22 @@ HTML * @return bool false if output is done, true if the rest of the form should be displayed */ function attemptSave() { - global $wgUser, $wgOut, $wgTitle; + global $wgUser, $wgOut; $resultDetails = false; # Allow bots to exempt some edits from bot flagging $bot = $wgUser->isAllowed( 'bot' ) && $this->bot; - $value = $this->internalAttemptSave( $resultDetails, $bot ); + $status = $this->internalAttemptSave( $resultDetails, $bot ); + // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status - if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { + if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) { $this->didSave = true; } - switch ( $value ) { + switch ( $status->value ) { case self::AS_HOOK_ERROR_EXPECTED: case self::AS_CONTENT_TOO_BIG: - case self::AS_ARTICLE_WAS_DELETED: + case self::AS_ARTICLE_WAS_DELETED: case self::AS_CONFLICT_DETECTED: case self::AS_SUMMARY_NEEDED: case self::AS_TEXTBOX_EMPTY: @@ -2672,8 +2872,28 @@ HTML case self::AS_HOOK_ERROR: case self::AS_FILTERING: + return false; + case self::AS_SUCCESS_NEW_ARTICLE: + $query = $resultDetails['redirect'] ? 'redirect=no' : ''; + $wgOut->redirect( $this->mTitle->getFullURL( $query ) ); + return false; + case self::AS_SUCCESS_UPDATE: + $extraQuery = ''; + $sectionanchor = $resultDetails['sectionanchor']; + + // Give extensions a chance to modify URL query on update + wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this->mArticle, &$sectionanchor, &$extraQuery ) ); + + if ( $resultDetails['redirect'] ) { + if ( $extraQuery == '' ) { + $extraQuery = 'redirect=no'; + } else { + $extraQuery = 'redirect=no&' . $extraQuery; + } + } + $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor ); return false; case self::AS_SPAM_ERROR: @@ -2692,22 +2912,22 @@ HTML $this->userNotLoggedInPage(); return false; - case self::AS_READ_ONLY_PAGE_LOGGED: - case self::AS_READ_ONLY_PAGE: - $wgOut->readOnlyPage(); - return false; + case self::AS_READ_ONLY_PAGE_LOGGED: + case self::AS_READ_ONLY_PAGE: + $wgOut->readOnlyPage(); + return false; - case self::AS_RATE_LIMITED: - $wgOut->rateLimited(); - return false; + case self::AS_RATE_LIMITED: + $wgOut->rateLimited(); + return false; - case self::AS_NO_CREATE_PERMISSION: - $this->noCreatePermission(); - return; + case self::AS_NO_CREATE_PERMISSION: + $this->noCreatePermission(); + return false; case self::AS_BLANK_ARTICLE: - $wgOut->redirect( $wgTitle->getFullURL() ); - return false; + $wgOut->redirect( $this->getContextTitle()->getFullURL() ); + return false; case self::AS_IMAGE_REDIRECT_LOGGED: $wgOut->permissionRequired( 'upload' ); @@ -2715,6 +2935,9 @@ HTML } } + /** + * @return Revision + */ function getBaseRevision() { if ( !$this->mBaseRevision ) { $db = wfGetDB( DB_MASTER ); diff --git a/includes/Exception.php b/includes/Exception.php index ff5d4b19..1f599d66 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -22,7 +22,7 @@ class MWException extends Exception { function useOutputPage() { return $this->useMessageCache() && !empty( $GLOBALS['wgFullyInitialised'] ) && - ( !empty( $GLOBALS['wgArticle'] ) || ( !empty( $GLOBALS['wgOut'] ) && !$GLOBALS['wgOut']->isArticle() ) ) && + !empty( $GLOBALS['wgOut'] ) && !empty( $GLOBALS['wgTitle'] ); } @@ -39,7 +39,7 @@ class MWException extends Exception { } } - return is_object( $wgLang ); + return $wgLang instanceof Language; } /** @@ -88,7 +88,7 @@ class MWException extends Exception { $args = array_slice( func_get_args(), 2 ); if ( $this->useMessageCache() ) { - return wfMsgReal( $key, $args ); + return wfMsgNoTrans( $key, $args ); } else { return wfMsgReplaceArgs( $fallback, $args ); } @@ -133,13 +133,8 @@ class MWException extends Exception { /* Return titles of this error page */ function getPageTitle() { - if ( $this->useMessageCache() ) { - return wfMsg( 'internalerror' ); - } else { - global $wgSitename; - - return "$wgSitename error"; - } + global $wgSitename; + return $this->msg( 'internalerror', "$wgSitename error" ); } /** @@ -155,7 +150,7 @@ class MWException extends Exception { $line = $this->getLine(); $message = $this->getMessage(); - if ( isset( $wgRequest ) ) { + if ( isset( $wgRequest ) && !$wgRequest instanceof FauxRequest ) { $url = $wgRequest->getRequestURL(); if ( !$url ) { $url = '[no URL]'; @@ -170,7 +165,6 @@ class MWException extends Exception { /** Output the exception report using HTML */ function reportHTML() { global $wgOut; - if ( $this->useOutputPage() ) { $wgOut->setPageTitle( $this->getPageTitle() ); $wgOut->setRobotPolicy( "noindex,nofollow" ); @@ -193,13 +187,8 @@ class MWException extends Exception { die( $hookResult ); } - if ( defined( 'MEDIAWIKI_INSTALL' ) || $this->htmlBodyOnly() ) { - echo $this->getHTML(); - } else { - echo $this->htmlHeader(); - echo $this->getHTML(); - echo $this->htmlFooter(); - } + echo $this->getHTML(); + die(1); } } @@ -215,55 +204,14 @@ class MWException extends Exception { } if ( self::isCommandLine() ) { - wfPrintError( $this->getText() ); + MWExceptionHandler::printError( $this->getText() ); } else { $this->reportHTML(); } } - /** - * Send headers and output the beginning of the html page if not using - * $wgOut to output the exception. - */ - function htmlHeader() { - global $wgLogo, $wgOutputEncoding; - - if ( !headers_sent() ) { - header( 'HTTP/1.0 500 Internal Server Error' ); - header( 'Content-type: text/html; charset=' . $wgOutputEncoding ); - /* Don't cache error pages! They cause no end of trouble... */ - header( 'Cache-control: none' ); - header( 'Pragma: nocache' ); - } - - $logo = htmlspecialchars( $wgLogo, ENT_QUOTES ); - $title = htmlspecialchars( $this->getPageTitle() ); - - return " - - $title - - -

    $title

    - "; - } - - /** - * print the end of the html page if not using $wgOut. - */ - function htmlFooter() { - return ""; - } - - /** - * headers handled by subclass? - */ - function htmlBodyOnly() { - return false; - } - static function isCommandLine() { - return !empty( $GLOBALS['wgCommandLineMode'] ) && !defined( 'MEDIAWIKI_INSTALL' ); + return !empty( $GLOBALS['wgCommandLineMode'] ); } } @@ -283,122 +231,253 @@ class FatalError extends MWException { } /** + * An error page which can definitely be safely rendered using the OutputPage * @ingroup Exception */ class ErrorPageError extends MWException { - public $title, $msg; + public $title, $msg, $params; /** * Note: these arguments are keys into wfMsg(), not text! */ - function __construct( $title, $msg ) { + function __construct( $title, $msg, $params = null ) { $this->title = $title; $this->msg = $msg; - parent::__construct( wfMsg( $msg ) ); + $this->params = $params; + + if( $msg instanceof Message ){ + parent::__construct( $msg ); + } else { + parent::__construct( wfMsg( $msg ) ); + } } function report() { global $wgOut; - $wgOut->showErrorPage( $this->title, $this->msg ); + $wgOut->showErrorPage( $this->title, $this->msg, $this->params ); $wgOut->output(); } } /** - * Install an exception handler for MediaWiki exception types. + * Show an error when a user tries to do something they do not have the necessary + * permissions for. + * @ingroup Exception */ -function wfInstallExceptionHandler() { - set_exception_handler( 'wfExceptionHandler' ); +class PermissionsError extends ErrorPageError { + public $permission; + + function __construct( $permission ) { + global $wgLang; + + $this->permission = $permission; + + $groups = array_map( + array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $this->permission ) + ); + + if( $groups ) { + parent::__construct( + 'badaccess', + 'badaccess-groups', + array( + $wgLang->commaList( $groups ), + count( $groups ) + ) + ); + } else { + parent::__construct( + 'badaccess', + 'badaccess-group0' + ); + } + } } /** - * Report an exception to the user + * Show an error when the wiki is locked/read-only and the user tries to do + * something that requires write access + * @ingroup Exception */ -function wfReportException( Exception $e ) { - global $wgShowExceptionDetails; +class ReadOnlyError extends ErrorPageError { + public function __construct(){ + parent::__construct( + 'readonly', + 'readonlytext', + wfReadOnlyReason() + ); + } +} - $cmdLine = MWException::isCommandLine(); +/** + * Show an error when the user hits a rate limit + * @ingroup Exception + */ +class ThrottledError extends ErrorPageError { + public function __construct(){ + parent::__construct( + 'actionthrottled', + 'actionthrottledtext' + ); + } + public function report(){ + global $wgOut; + $wgOut->setStatusCode( 503 ); + return parent::report(); + } +} - if ( $e instanceof MWException ) { - try { - $e->report(); - } catch ( Exception $e2 ) { - // Exception occurred from within exception handler - // Show a simpler error message for the original exception, - // don't try to invoke report() - $message = "MediaWiki internal error.\n\n"; +/** + * Show an error when the user tries to do something whilst blocked + * @ingroup Exception + */ +class UserBlockedError extends ErrorPageError { + public function __construct( Block $block ){ + global $wgLang; - if ( $wgShowExceptionDetails ) { - $message .= 'Original exception: ' . $e->__toString() . "\n\n" . - 'Exception caught inside exception handler: ' . $e2->__toString(); - } else { - $message .= "Exception caught inside exception handler.\n\n" . - "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " . - "to show detailed debugging information."; + $blockerUserpage = $block->getBlocker()->getUserPage(); + $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerUserpage->getText()}]]"; + + $reason = $block->mReason; + if( $reason == '' ) { + $reason = wfMsg( 'blockednoreason' ); + } + + /* $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 = $block->getTarget(); + + parent::__construct( + 'blockedtitle', + $block->mAuto ? 'autoblockedtext' : 'blockedtext', + array( + $link, + $reason, + wfGetIP(), + $block->getBlocker()->getName(), + $block->getId(), + $wgLang->formatExpiry( $block->mExpiry ), + $intended, + $wgLang->timeanddate( wfTimestamp( TS_MW, $block->mTimestamp ), true ) + ) + ); + } +} + +/** + * Handler class for MWExceptions + * @ingroup Exception + */ +class MWExceptionHandler { + /** + * Install an exception handler for MediaWiki exception types. + */ + public static function installHandler() { + set_exception_handler( array( 'MWExceptionHandler', 'handle' ) ); + } + + /** + * Report an exception to the user + */ + protected static function report( Exception $e ) { + global $wgShowExceptionDetails; + + $cmdLine = MWException::isCommandLine(); + + if ( $e instanceof MWException ) { + try { + // Try and show the exception prettily, with the normal skin infrastructure + $e->report(); + } catch ( Exception $e2 ) { + // Exception occurred from within exception handler + // Show a simpler error message for the original exception, + // don't try to invoke report() + $message = "MediaWiki internal error.\n\n"; + + if ( $wgShowExceptionDetails ) { + $message .= 'Original exception: ' . $e->__toString() . "\n\n" . + 'Exception caught inside exception handler: ' . $e2->__toString(); + } else { + $message .= "Exception caught inside exception handler.\n\n" . + "Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " . + "to show detailed debugging information."; + } + + $message .= "\n"; + + if ( $cmdLine ) { + self::printError( $message ); + } else { + self::escapeEchoAndDie( $message ); + } } + } else { + $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" . + $e->__toString() . "\n"; - $message .= "\n"; + if ( $wgShowExceptionDetails ) { + $message .= "\n" . $e->getTraceAsString() . "\n"; + } if ( $cmdLine ) { - wfPrintError( $message ); + self::printError( $message ); } else { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; + self::escapeEchoAndDie( $message ); } } - } else { - $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" . - $e->__toString() . "\n"; - - if ( $wgShowExceptionDetails ) { - $message .= "\n" . $e->getTraceAsString() . "\n"; - } + } - if ( $cmdLine ) { - wfPrintError( $message ); + /** + * Print a message, if possible to STDERR. + * Use this in command line mode only (see isCommandLine) + * @param $message String Failure text + */ + public static function printError( $message ) { + # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). + # Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though. + if ( defined( 'STDERR' ) ) { + fwrite( STDERR, $message ); } else { - echo nl2br( htmlspecialchars( $message ) ) . "\n"; + echo( $message ); } } -} -/** - * Print a message, if possible to STDERR. - * Use this in command line mode only (see isCommandLine) - */ -function wfPrintError( $message ) { - # NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). - # Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though. - if ( defined( 'STDERR' ) ) { - fwrite( STDERR, $message ); - } else { - echo( $message ); + /** + * Print a message after escaping it and converting newlines to
    + * Use this for non-command line failures + * @param $message String Failure text + */ + private static function escapeEchoAndDie( $message ) { + echo nl2br( htmlspecialchars( $message ) ) . "\n"; + die(1); } -} -/** - * Exception handler which simulates the appropriate catch() handling: - * - * try { - * ... - * } catch ( MWException $e ) { - * $e->report(); - * } catch ( Exception $e ) { - * echo $e->__toString(); - * } - */ -function wfExceptionHandler( $e ) { - global $wgFullyInitialised; + /** + * Exception handler which simulates the appropriate catch() handling: + * + * try { + * ... + * } catch ( MWException $e ) { + * $e->report(); + * } catch ( Exception $e ) { + * echo $e->__toString(); + * } + */ + public static function handle( $e ) { + global $wgFullyInitialised; - wfReportException( $e ); + self::report( $e ); - // Final cleanup - if ( $wgFullyInitialised ) { - try { - wfLogProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition - } catch ( Exception $e ) {} - } + // Final cleanup + if ( $wgFullyInitialised ) { + try { + wfLogProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition + } catch ( Exception $e ) {} + } - // Exit value should be nonzero for the benefit of shell jobs - exit( 1 ); + // Exit value should be nonzero for the benefit of shell jobs + exit( 1 ); + } } diff --git a/includes/Exif.php b/includes/Exif.php deleted file mode 100644 index 630821c3..00000000 --- a/includes/Exif.php +++ /dev/null @@ -1,1150 +0,0 @@ - - * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License - * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification - * @file - */ - -/** - * @todo document (e.g. one-sentence class-overview description) - * @ingroup Media - */ -class Exif { - - const BYTE = 1; //!< An 8-bit (1-byte) unsigned integer. - const ASCII = 2; //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. - const SHORT = 3; //!< A 16-bit (2-byte) unsigned integer. - const LONG = 4; //!< A 32-bit (4-byte) unsigned integer. - const RATIONAL = 5; //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator - const UNDEFINED = 7; //!< An 8-bit byte that can take any value depending on the field definition - const SLONG = 9; //!< A 32-bit (4-byte) signed integer (2's complement notation), - const SRATIONAL = 10; //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. - - //@{ - /* @var array - * @private - */ - - /** - * Exif tags grouped by category, the tagname itself is the key and the type - * is the value, in the case of more than one possible value type they are - * separated by commas. - */ - var $mExifTags; - - /** - * A one dimentional array of all Exif tags - */ - var $mFlatExifTags; - - /** - * The raw Exif data returned by exif_read_data() - */ - var $mRawExifData; - - /** - * A Filtered version of $mRawExifData that has been pruned of invalid - * tags and tags that contain content they shouldn't contain according - * to the Exif specification - */ - var $mFilteredExifData; - - /** - * Filtered and formatted Exif data, see FormatExif::getFormattedData() - */ - var $mFormattedExifData; - - //@} - - //@{ - /* @var string - * @private - */ - - /** - * The file being processed - */ - var $file; - - /** - * The basename of the file being processed - */ - var $basename; - - /** - * The private log to log to, e.g. 'exif' - */ - var $log = false; - - //@} - - /** - * Constructor - * - * @param $file String: filename. - */ - function __construct( $file ) { - /** - * Page numbers here refer to pages in the EXIF 2.2 standard - * - * @link http://exif.org/Exif2-2.PDF The Exif 2.2 specification - */ - $this->mExifTags = array( - # TIFF Rev. 6.0 Attribute Information (p22) - 'tiff' => array( - # Tags relating to image structure - 'structure' => array( - 'ImageWidth' => Exif::SHORT.','.Exif::LONG, # Image width - 'ImageLength' => Exif::SHORT.','.Exif::LONG, # Image height - 'BitsPerSample' => Exif::SHORT, # Number of bits per component - # "When a primary image is JPEG compressed, this designation is not" - # "necessary and is omitted." (p23) - 'Compression' => Exif::SHORT, # Compression scheme #p23 - 'PhotometricInterpretation' => Exif::SHORT, # Pixel composition #p23 - 'Orientation' => Exif::SHORT, # Orientation of image #p24 - 'SamplesPerPixel' => Exif::SHORT, # Number of components - 'PlanarConfiguration' => Exif::SHORT, # Image data arrangement #p24 - 'YCbCrSubSampling' => Exif::SHORT, # Subsampling ratio of Y to C #p24 - 'YCbCrPositioning' => Exif::SHORT, # Y and C positioning #p24-25 - 'XResolution' => Exif::RATIONAL, # Image resolution in width direction - 'YResolution' => Exif::RATIONAL, # Image resolution in height direction - 'ResolutionUnit' => Exif::SHORT, # Unit of X and Y resolution #(p26) - ), - - # Tags relating to recording offset - 'offset' => array( - 'StripOffsets' => Exif::SHORT.','.Exif::LONG, # Image data location - 'RowsPerStrip' => Exif::SHORT.','.Exif::LONG, # Number of rows per strip - 'StripByteCounts' => Exif::SHORT.','.Exif::LONG, # Bytes per compressed strip - 'JPEGInterchangeFormat' => Exif::SHORT.','.Exif::LONG, # Offset to JPEG SOI - 'JPEGInterchangeFormatLength' => Exif::SHORT.','.Exif::LONG, # Bytes of JPEG data - ), - - # Tags relating to image data characteristics - 'characteristics' => array( - 'TransferFunction' => Exif::SHORT, # Transfer function - 'WhitePoint' => Exif::RATIONAL, # White point chromaticity - 'PrimaryChromaticities' => Exif::RATIONAL, # Chromaticities of primarities - 'YCbCrCoefficients' => Exif::RATIONAL, # Color space transformation matrix coefficients #p27 - 'ReferenceBlackWhite' => Exif::RATIONAL # Pair of black and white reference values - ), - - # Other tags - 'other' => array( - 'DateTime' => Exif::ASCII, # File change date and time - 'ImageDescription' => Exif::ASCII, # Image title - 'Make' => Exif::ASCII, # Image input equipment manufacturer - 'Model' => Exif::ASCII, # Image input equipment model - 'Software' => Exif::ASCII, # Software used - 'Artist' => Exif::ASCII, # Person who created the image - 'Copyright' => Exif::ASCII, # Copyright holder - ), - ), - - # Exif IFD Attribute Information (p30-31) - 'exif' => array( - # Tags relating to version - 'version' => array( - # TODO: NOTE: Nonexistence of this field is taken to mean nonconformance - # to the EXIF 2.1 AND 2.2 standards - 'ExifVersion' => Exif::UNDEFINED, # Exif version - 'FlashpixVersion' => Exif::UNDEFINED, # Supported Flashpix version #p32 - ), - - # Tags relating to Image Data Characteristics - 'characteristics' => array( - 'ColorSpace' => Exif::SHORT, # Color space information #p32 - ), - - # Tags relating to image configuration - 'configuration' => array( - 'ComponentsConfiguration' => Exif::UNDEFINED, # Meaning of each component #p33 - 'CompressedBitsPerPixel' => Exif::RATIONAL, # Image compression mode - 'PixelYDimension' => Exif::SHORT.','.Exif::LONG, # Valid image width - 'PixelXDimension' => Exif::SHORT.','.Exif::LONG, # Valind image height - ), - - # Tags relating to related user information - 'user' => array( - 'MakerNote' => Exif::UNDEFINED, # Manufacturer notes - 'UserComment' => Exif::UNDEFINED, # User comments #p34 - ), - - # Tags relating to related file information - 'related' => array( - 'RelatedSoundFile' => Exif::ASCII, # Related audio file - ), - - # Tags relating to date and time - 'dateandtime' => array( - 'DateTimeOriginal' => Exif::ASCII, # Date and time of original data generation #p36 - 'DateTimeDigitized' => Exif::ASCII, # Date and time of original data generation - 'SubSecTime' => Exif::ASCII, # DateTime subseconds - 'SubSecTimeOriginal' => Exif::ASCII, # DateTimeOriginal subseconds - 'SubSecTimeDigitized' => Exif::ASCII, # DateTimeDigitized subseconds - ), - - # Tags relating to picture-taking conditions (p31) - 'conditions' => array( - 'ExposureTime' => Exif::RATIONAL, # Exposure time - 'FNumber' => Exif::RATIONAL, # F Number - 'ExposureProgram' => Exif::SHORT, # Exposure Program #p38 - 'SpectralSensitivity' => Exif::ASCII, # Spectral sensitivity - 'ISOSpeedRatings' => Exif::SHORT, # ISO speed rating - 'OECF' => Exif::UNDEFINED, # Optoelectronic conversion factor - 'ShutterSpeedValue' => Exif::SRATIONAL, # Shutter speed - 'ApertureValue' => Exif::RATIONAL, # Aperture - 'BrightnessValue' => Exif::SRATIONAL, # Brightness - 'ExposureBiasValue' => Exif::SRATIONAL, # Exposure bias - 'MaxApertureValue' => Exif::RATIONAL, # Maximum land aperture - 'SubjectDistance' => Exif::RATIONAL, # Subject distance - 'MeteringMode' => Exif::SHORT, # Metering mode #p40 - 'LightSource' => Exif::SHORT, # Light source #p40-41 - 'Flash' => Exif::SHORT, # Flash #p41-42 - 'FocalLength' => Exif::RATIONAL, # Lens focal length - 'SubjectArea' => Exif::SHORT, # Subject area - 'FlashEnergy' => Exif::RATIONAL, # Flash energy - 'SpatialFrequencyResponse' => Exif::UNDEFINED, # Spatial frequency response - 'FocalPlaneXResolution' => Exif::RATIONAL, # Focal plane X resolution - 'FocalPlaneYResolution' => Exif::RATIONAL, # Focal plane Y resolution - 'FocalPlaneResolutionUnit' => Exif::SHORT, # Focal plane resolution unit #p46 - 'SubjectLocation' => Exif::SHORT, # Subject location - 'ExposureIndex' => Exif::RATIONAL, # Exposure index - 'SensingMethod' => Exif::SHORT, # Sensing method #p46 - 'FileSource' => Exif::UNDEFINED, # File source #p47 - 'SceneType' => Exif::UNDEFINED, # Scene type #p47 - 'CFAPattern' => Exif::UNDEFINED, # CFA pattern - 'CustomRendered' => Exif::SHORT, # Custom image processing #p48 - 'ExposureMode' => Exif::SHORT, # Exposure mode #p48 - 'WhiteBalance' => Exif::SHORT, # White Balance #p49 - 'DigitalZoomRatio' => Exif::RATIONAL, # Digital zoom ration - 'FocalLengthIn35mmFilm' => Exif::SHORT, # Focal length in 35 mm film - 'SceneCaptureType' => Exif::SHORT, # Scene capture type #p49 - 'GainControl' => Exif::RATIONAL, # Scene control #p49-50 - 'Contrast' => Exif::SHORT, # Contrast #p50 - 'Saturation' => Exif::SHORT, # Saturation #p50 - 'Sharpness' => Exif::SHORT, # Sharpness #p50 - 'DeviceSettingDescription' => Exif::UNDEFINED, # Desice settings description - 'SubjectDistanceRange' => Exif::SHORT, # Subject distance range #p51 - ), - - 'other' => array( - 'ImageUniqueID' => Exif::ASCII, # Unique image ID - ), - ), - - # GPS Attribute Information (p52) - 'gps' => array( - 'GPSVersionID' => Exif::BYTE, # GPS tag version - 'GPSLatitudeRef' => Exif::ASCII, # North or South Latitude #p52-53 - 'GPSLatitude' => Exif::RATIONAL, # Latitude - 'GPSLongitudeRef' => Exif::ASCII, # East or West Longitude #p53 - 'GPSLongitude' => Exif::RATIONAL, # Longitude - 'GPSAltitudeRef' => Exif::BYTE, # Altitude reference - 'GPSAltitude' => Exif::RATIONAL, # Altitude - 'GPSTimeStamp' => Exif::RATIONAL, # GPS time (atomic clock) - 'GPSSatellites' => Exif::ASCII, # Satellites used for measurement - 'GPSStatus' => Exif::ASCII, # Receiver status #p54 - 'GPSMeasureMode' => Exif::ASCII, # Measurement mode #p54-55 - 'GPSDOP' => Exif::RATIONAL, # Measurement precision - 'GPSSpeedRef' => Exif::ASCII, # Speed unit #p55 - 'GPSSpeed' => Exif::RATIONAL, # Speed of GPS receiver - 'GPSTrackRef' => Exif::ASCII, # Reference for direction of movement #p55 - 'GPSTrack' => Exif::RATIONAL, # Direction of movement - 'GPSImgDirectionRef' => Exif::ASCII, # Reference for direction of image #p56 - 'GPSImgDirection' => Exif::RATIONAL, # Direction of image - 'GPSMapDatum' => Exif::ASCII, # Geodetic survey data used - 'GPSDestLatitudeRef' => Exif::ASCII, # Reference for latitude of destination #p56 - 'GPSDestLatitude' => Exif::RATIONAL, # Latitude destination - 'GPSDestLongitudeRef' => Exif::ASCII, # Reference for longitude of destination #p57 - 'GPSDestLongitude' => Exif::RATIONAL, # Longitude of destination - 'GPSDestBearingRef' => Exif::ASCII, # Reference for bearing of destination #p57 - 'GPSDestBearing' => Exif::RATIONAL, # Bearing of destination - 'GPSDestDistanceRef' => Exif::ASCII, # Reference for distance to destination #p57-58 - 'GPSDestDistance' => Exif::RATIONAL, # Distance to destination - 'GPSProcessingMethod' => Exif::UNDEFINED, # Name of GPS processing method - 'GPSAreaInformation' => Exif::UNDEFINED, # Name of GPS area - 'GPSDateStamp' => Exif::ASCII, # GPS date - 'GPSDifferential' => Exif::SHORT, # GPS differential correction - ), - ); - - $this->file = $file; - $this->basename = wfBaseName( $this->file ); - - $this->makeFlatExifTags(); - - $this->debugFile( $this->basename, __FUNCTION__, true ); - wfSuppressWarnings(); - $data = exif_read_data( $this->file ); - wfRestoreWarnings(); - /** - * exif_read_data() will return false on invalid input, such as - * when somebody uploads a file called something.jpeg - * containing random gibberish. - */ - $this->mRawExifData = $data ? $data : array(); - - $this->makeFilteredData(); - $this->makeFormattedData(); - - $this->debugFile( __FUNCTION__, false ); - } - - /**#@+ - * @private - */ - /** - * Generate a flat list of the exif tags - */ - function makeFlatExifTags() { - $this->extractTags( $this->mExifTags ); - } - - /** - * A recursing extractor function used by makeFlatExifTags() - * - * Note: This used to use an array_walk function, but it made PHP5 - * segfault, see `cvs diff -u -r 1.4 -r 1.5 Exif.php` - */ - function extractTags( &$tagset ) { - foreach( $tagset as $key => $val ) { - if( is_array( $val ) ) { - $this->extractTags( $val ); - } else { - $this->mFlatExifTags[$key] = $val; - } - } - } - - /** - * Make $this->mFilteredExifData - */ - function makeFilteredData() { - $this->mFilteredExifData = $this->mRawExifData; - - foreach( $this->mFilteredExifData as $k => $v ) { - if ( !in_array( $k, array_keys( $this->mFlatExifTags ) ) ) { - $this->debug( $v, __FUNCTION__, "'$k' is not a valid Exif tag" ); - unset( $this->mFilteredExifData[$k] ); - } - } - - foreach( $this->mFilteredExifData as $k => $v ) { - if ( !$this->validate($k, $v) ) { - $this->debug( $v, __FUNCTION__, "'$k' contained invalid data" ); - unset( $this->mFilteredExifData[$k] ); - } - } - } - - /** - * @todo document - */ - function makeFormattedData( ) { - $format = new FormatExif( $this->getFilteredData() ); - $this->mFormattedExifData = $format->getFormattedData(); - } - /**#@-*/ - - /**#@+ - * @return array - */ - /** - * Get $this->mRawExifData - */ - function getData() { - return $this->mRawExifData; - } - - /** - * Get $this->mFilteredExifData - */ - function getFilteredData() { - return $this->mFilteredExifData; - } - - /** - * Get $this->mFormattedExifData - */ - function getFormattedData() { - return $this->mFormattedExifData; - } - /**#@-*/ - - /** - * The version of the output format - * - * Before the actual metadata information is saved in the database we - * strip some of it since we don't want to save things like thumbnails - * which usually accompany Exif data. This value gets saved in the - * database along with the actual Exif data, and if the version in the - * database doesn't equal the value returned by this function the Exif - * data is regenerated. - * - * @return int - */ - public static function version() { - return 1; // We don't need no bloddy constants! - } - - /**#@+ - * Validates if a tag value is of the type it should be according to the Exif spec - * - * @private - * - * @param $in Mixed: the input value to check - * @return bool - */ - function isByte( $in ) { - if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 255 ) { - $this->debug( $in, __FUNCTION__, true ); - return true; - } else { - $this->debug( $in, __FUNCTION__, false ); - return false; - } - } - - function isASCII( $in ) { - if ( is_array( $in ) ) { - return false; - } - - if ( preg_match( "/[^\x0a\x20-\x7e]/", $in ) ) { - $this->debug( $in, __FUNCTION__, 'found a character not in our whitelist' ); - return false; - } - - if ( preg_match( '/^\s*$/', $in ) ) { - $this->debug( $in, __FUNCTION__, 'input consisted solely of whitespace' ); - return false; - } - - return true; - } - - function isShort( $in ) { - if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 65536 ) { - $this->debug( $in, __FUNCTION__, true ); - return true; - } else { - $this->debug( $in, __FUNCTION__, false ); - return false; - } - } - - function isLong( $in ) { - if ( !is_array( $in ) && sprintf('%d', $in) == $in && $in >= 0 && $in <= 4294967296 ) { - $this->debug( $in, __FUNCTION__, true ); - return true; - } else { - $this->debug( $in, __FUNCTION__, false ); - return false; - } - } - - function isRational( $in ) { - $m = array(); - if ( !is_array( $in ) && @preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero - return $this->isLong( $m[1] ) && $this->isLong( $m[2] ); - } else { - $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); - return false; - } - } - - function isUndefined( $in ) { - if ( !is_array( $in ) && preg_match( '/^\d{4}$/', $in ) ) { // Allow ExifVersion and FlashpixVersion - $this->debug( $in, __FUNCTION__, true ); - return true; - } else { - $this->debug( $in, __FUNCTION__, false ); - return false; - } - } - - function isSlong( $in ) { - if ( $this->isLong( abs( $in ) ) ) { - $this->debug( $in, __FUNCTION__, true ); - return true; - } else { - $this->debug( $in, __FUNCTION__, false ); - return false; - } - } - - function isSrational( $in ) { - $m = array(); - if ( !is_array( $in ) && preg_match( '/^(\d+)\/(\d+[1-9]|[1-9]\d*)$/', $in, $m ) ) { # Avoid division by zero - return $this->isSlong( $m[0] ) && $this->isSlong( $m[1] ); - } else { - $this->debug( $in, __FUNCTION__, 'fed a non-fraction value' ); - return false; - } - } - /**#@-*/ - - /** - * Validates if a tag has a legal value according to the Exif spec - * - * @private - * - * @param $tag String: the tag to check. - * @param $val Mixed: the value of the tag. - * @return bool - */ - function validate( $tag, $val ) { - $debug = "tag is '$tag'"; - // Does not work if not typecast - switch( (string)$this->mFlatExifTags[$tag] ) { - case (string)Exif::BYTE: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isByte( $val ); - case (string)Exif::ASCII: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isASCII( $val ); - case (string)Exif::SHORT: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isShort( $val ); - case (string)Exif::LONG: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isLong( $val ); - case (string)Exif::RATIONAL: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isRational( $val ); - case (string)Exif::UNDEFINED: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isUndefined( $val ); - case (string)Exif::SLONG: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isSlong( $val ); - case (string)Exif::SRATIONAL: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isSrational( $val ); - case (string)Exif::SHORT.','.Exif::LONG: - $this->debug( $val, __FUNCTION__, $debug ); - return $this->isShort( $val ) || $this->isLong( $val ); - default: - $this->debug( $val, __FUNCTION__, "The tag '$tag' is unknown" ); - return false; - } - } - - /** - * Convenience function for debugging output - * - * @private - * - * @param $in Mixed: - * @param $fname String: - * @param $action Mixed: , default NULL. - */ - function debug( $in, $fname, $action = null ) { - if ( !$this->log ) { - return; - } - $type = gettype( $in ); - $class = ucfirst( __CLASS__ ); - if ( $type === 'array' ) - $in = print_r( $in, true ); - - if ( $action === true ) - wfDebugLog( $this->log, "$class::$fname: accepted: '$in' (type: $type)\n"); - elseif ( $action === false ) - wfDebugLog( $this->log, "$class::$fname: rejected: '$in' (type: $type)\n"); - elseif ( $action === null ) - wfDebugLog( $this->log, "$class::$fname: input was: '$in' (type: $type)\n"); - else - wfDebugLog( $this->log, "$class::$fname: $action (type: $type; content: '$in')\n"); - } - - /** - * Convenience function for debugging output - * - * @private - * - * @param $fname String: the name of the function calling this function - * @param $io Boolean: Specify whether we're beginning or ending - */ - function debugFile( $fname, $io ) { - if ( !$this->log ) { - return; - } - $class = ucfirst( __CLASS__ ); - if ( $io ) { - wfDebugLog( $this->log, "$class::$fname: begin processing: '{$this->basename}'\n" ); - } else { - wfDebugLog( $this->log, "$class::$fname: end processing: '{$this->basename}'\n" ); - } - } - -} - -/** - * @todo document (e.g. one-sentence class-overview description) - * @ingroup Media - */ -class FormatExif { - /** - * The Exif data to format - * - * @var array - * @private - */ - var $mExif; - - /** - * Constructor - * - * @param $exif Array: the Exif data to format ( as returned by - * Exif::getFilteredData() ) - */ - function __construct( $exif ) { - $this->mExif = $exif; - } - - /** - * Numbers given by Exif user agents are often magical, that is they - * should be replaced by a detailed explanation depending on their - * value which most of the time are plain integers. This function - * formats Exif values into human readable form. - * - * @return array - */ - function getFormattedData() { - global $wgLang; - - $tags =& $this->mExif; - - $resolutionunit = !isset( $tags['ResolutionUnit'] ) || $tags['ResolutionUnit'] == 2 ? 2 : 3; - unset( $tags['ResolutionUnit'] ); - - foreach( $tags as $tag => $val ) { - switch( $tag ) { - case 'Compression': - switch( $val ) { - case 1: case 6: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'PhotometricInterpretation': - switch( $val ) { - case 2: case 6: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'Orientation': - switch( $val ) { - case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'PlanarConfiguration': - switch( $val ) { - case 1: case 2: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - // TODO: YCbCrSubSampling - // TODO: YCbCrPositioning - - case 'XResolution': - case 'YResolution': - switch( $resolutionunit ) { - case 2: - $tags[$tag] = $this->msg( 'XYResolution', 'i', $this->formatNum( $val ) ); - break; - case 3: - $this->msg( 'XYResolution', 'c', $this->formatNum( $val ) ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - // TODO: YCbCrCoefficients #p27 (see annex E) - case 'ExifVersion': case 'FlashpixVersion': - $tags[$tag] = "$val"/100; - break; - - case 'ColorSpace': - switch( $val ) { - case 1: case 'FFFF.H': - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'ComponentsConfiguration': - switch( $val ) { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'DateTime': - case 'DateTimeOriginal': - case 'DateTimeDigitized': - if( $val == '0000:00:00 00:00:00' ) { - $tags[$tag] = wfMsg('exif-unknowndate'); - } elseif( preg_match( '/^(?:\d{4}):(?:\d\d):(?:\d\d) (?:\d\d):(?:\d\d):(?:\d\d)$/', $val ) ) { - $tags[$tag] = $wgLang->timeanddate( wfTimestamp(TS_MW, $val) ); - } - break; - - case 'ExposureProgram': - switch( $val ) { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'SubjectDistance': - $tags[$tag] = $this->msg( $tag, '', $this->formatNum( $val ) ); - break; - - case 'MeteringMode': - switch( $val ) { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 255: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'LightSource': - switch( $val ) { - case 0: case 1: case 2: case 3: case 4: case 9: case 10: case 11: - case 12: case 13: case 14: case 15: case 17: case 18: case 19: case 20: - case 21: case 22: case 23: case 24: case 255: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'Flash': - $flashDecode = array( - 'fired' => $val & bindec( '00000001' ), - 'return' => ($val & bindec( '00000110' )) >> 1, - 'mode' => ($val & bindec( '00011000' )) >> 3, - 'function' => ($val & bindec( '00100000' )) >> 5, - 'redeye' => ($val & bindec( '01000000' )) >> 6, -// 'reserved' => ($val & bindec( '10000000' )) >> 7, - ); - - # We do not need to handle unknown values since all are used. - foreach( $flashDecode as $subTag => $subValue ) { - # We do not need any message for zeroed values. - if( $subTag != 'fired' && $subValue == 0) { - continue; - } - $fullTag = $tag . '-' . $subTag ; - $flashMsgs[] = $this->msg( $fullTag, $subValue ); - } - $tags[$tag] = $wgLang->commaList( $flashMsgs ); - break; - - case 'FocalPlaneResolutionUnit': - switch( $val ) { - case 2: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'SensingMethod': - switch( $val ) { - case 1: case 2: case 3: case 4: case 5: case 7: case 8: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'FileSource': - switch( $val ) { - case 3: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'SceneType': - switch( $val ) { - case 1: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'CustomRendered': - switch( $val ) { - case 0: case 1: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'ExposureMode': - switch( $val ) { - case 0: case 1: case 2: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'WhiteBalance': - switch( $val ) { - case 0: case 1: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'SceneCaptureType': - switch( $val ) { - case 0: case 1: case 2: case 3: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GainControl': - switch( $val ) { - case 0: case 1: case 2: case 3: case 4: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'Contrast': - switch( $val ) { - case 0: case 1: case 2: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'Saturation': - switch( $val ) { - case 0: case 1: case 2: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'Sharpness': - switch( $val ) { - case 0: case 1: case 2: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'SubjectDistanceRange': - switch( $val ) { - case 0: case 1: case 2: case 3: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSLatitudeRef': - case 'GPSDestLatitudeRef': - switch( $val ) { - case 'N': case 'S': - $tags[$tag] = $this->msg( 'GPSLatitude', $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSLongitudeRef': - case 'GPSDestLongitudeRef': - switch( $val ) { - case 'E': case 'W': - $tags[$tag] = $this->msg( 'GPSLongitude', $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSStatus': - switch( $val ) { - case 'A': case 'V': - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSMeasureMode': - switch( $val ) { - case 2: case 3: - $tags[$tag] = $this->msg( $tag, $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSSpeedRef': - case 'GPSDestDistanceRef': - switch( $val ) { - case 'K': case 'M': case 'N': - $tags[$tag] = $this->msg( 'GPSSpeed', $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSTrackRef': - case 'GPSImgDirectionRef': - case 'GPSDestBearingRef': - switch( $val ) { - case 'T': case 'M': - $tags[$tag] = $this->msg( 'GPSDirection', $val ); - break; - default: - $tags[$tag] = $val; - break; - } - break; - - case 'GPSDateStamp': - $tags[$tag] = $wgLang->date( substr( $val, 0, 4 ) . substr( $val, 5, 2 ) . substr( $val, 8, 2 ) . '000000' ); - break; - - // This is not in the Exif standard, just a special - // case for our purposes which enables wikis to wikify - // the make, model and software name to link to their articles. - case 'Make': - case 'Model': - case 'Software': - $tags[$tag] = $this->msg( $tag, '', $val ); - break; - - case 'ExposureTime': - // Show the pretty fraction as well as decimal version - $tags[$tag] = wfMsg( 'exif-exposuretime-format', - $this->formatFraction( $val ), $this->formatNum( $val ) ); - break; - - case 'FNumber': - $tags[$tag] = wfMsg( 'exif-fnumber-format', - $this->formatNum( $val ) ); - break; - - case 'FocalLength': - $tags[$tag] = wfMsg( 'exif-focallength-format', - $this->formatNum( $val ) ); - break; - - // Do not transform fields with pure text. - // For some languages the formatNum() conversion results to wrong output like - // foo,bar@example,com or foo٫bar@example٫com - case 'ImageDescription': - case 'Artist': - case 'Copyright': - $tags[$tag] = htmlspecialchars( $val ); - break; - default: - $tags[$tag] = $this->formatNum( $val ); - break; - } - } - - return $tags; - } - - /** - * Convenience function for getFormattedData() - * - * @private - * - * @param $tag String: the tag name to pass on - * @param $val String: the value of the tag - * @param $arg String: an argument to pass ($1) - * @return string A wfMsg of "exif-$tag-$val" in lower case - */ - function msg( $tag, $val, $arg = null ) { - global $wgContLang; - - if ($val === '') - $val = 'value'; - return wfMsg( $wgContLang->lc( "exif-$tag-$val" ), $arg ); - } - - /** - * Format a number, convert numbers from fractions into floating point - * numbers - * - * @private - * - * @param $num Mixed: the value to format - * @return mixed A floating point number or whatever we were fed - */ - function formatNum( $num ) { - global $wgLang; - - $m = array(); - if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) - return $wgLang->formatNum( $m[2] != 0 ? $m[1] / $m[2] : $num ); - else - return $wgLang->formatNum( $num ); - } - - /** - * Format a rational number, reducing fractions - * - * @private - * - * @param $num Mixed: the value to format - * @return mixed A floating point number or whatever we were fed - */ - function formatFraction( $num ) { - $m = array(); - if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) ) { - $numerator = intval( $m[1] ); - $denominator = intval( $m[2] ); - $gcd = $this->gcd( $numerator, $denominator ); - if( $gcd != 0 ) { - // 0 shouldn't happen! ;) - return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd ); - } - } - return $this->formatNum( $num ); - } - - /** - * Calculate the greatest common divisor of two integers. - * - * @param $a Integer: FIXME - * @param $b Integer: FIXME - * @return int - * @private - */ - function gcd( $a, $b ) { - /* - // http://en.wikipedia.org/wiki/Euclidean_algorithm - // Recursive form would be: - if( $b == 0 ) - return $a; - else - return gcd( $b, $a % $b ); - */ - while( $b != 0 ) { - $remainder = $a % $b; - - // tail recursion... - $a = $b; - $b = $remainder; - } - return $a; - } -} diff --git a/includes/Export.php b/includes/Export.php index e7cd4d3f..87c735c1 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -35,11 +35,13 @@ class WikiExporter { var $author_list = "" ; var $dumpUploads = false; + var $dumpUploadFileContents = false; const FULL = 1; const CURRENT = 2; const STABLE = 4; // extension defined const LOGS = 8; + const RANGE = 16; const BUFFER = 0; const STREAM = 1; @@ -55,7 +57,8 @@ class WikiExporter { * main query is still running. * * @param $db Database - * @param $history Mixed: one of WikiExporter::FULL or WikiExporter::CURRENT, + * @param $history Mixed: one of WikiExporter::FULL, WikiExporter::CURRENT, + * WikiExporter::RANGE or WikiExporter::STABLE, * or an associative array: * offset: non-inclusive offset at which to start the query * limit: maximum number of rows to return @@ -112,12 +115,27 @@ class WikiExporter { */ public function pagesByRange( $start, $end ) { $condition = 'page_id >= ' . intval( $start ); - if( $end ) { + if ( $end ) { $condition .= ' AND page_id < ' . intval( $end ); } return $this->dumpFrom( $condition ); } + /** + * Dumps a series of page and revision records for those pages + * in the database with revisions falling within the rev_id range given. + * @param $start Int: inclusive lower limit (this id is included) + * @param $end Int: Exclusive upper limit (this id is not included) + * If 0, no upper limit. + */ + public function revsByRange( $start, $end ) { + $condition = 'rev_id >= ' . intval( $start ); + if ( $end ) { + $condition .= ' AND rev_id < ' . intval( $end ); + } + return $this->dumpFrom( $condition ); + } + /** * @param $title Title */ @@ -129,7 +147,7 @@ class WikiExporter { public function pageByName( $name ) { $title = Title::newFromText( $name ); - if( is_null( $title ) ) { + if ( is_null( $title ) ) { throw new MWException( "Can't export invalid title" ); } else { return $this->pageByTitle( $title ); @@ -137,7 +155,7 @@ class WikiExporter { } public function pagesByName( $names ) { - foreach( $names as $name ) { + foreach ( $names as $name ) { $this->pageByName( $name ); } } @@ -148,7 +166,7 @@ class WikiExporter { public function logsByRange( $start, $end ) { $condition = 'log_id >= ' . intval( $start ); - if( $end ) { + if ( $end ) { $condition .= ' AND log_id < ' . intval( $end ); } return $this->dumpFrom( $condition ); @@ -157,17 +175,23 @@ class WikiExporter { # Generates the distinct list of authors of an article # Not called by default (depends on $this->list_authors) # Can be set by Special:Export when not exporting whole history - protected function do_list_authors( $page , $revision , $cond ) { + protected function do_list_authors( $cond ) { wfProfileIn( __METHOD__ ); $this->author_list = ""; - //rev_deleted - $nothidden = '('.$this->db->bitAnd('rev_deleted', Revision::DELETED_USER) . ') = 0'; - - $sql = "SELECT DISTINCT rev_user_text,rev_user FROM {$page},{$revision} - WHERE page_id=rev_page AND $nothidden AND " . $cond ; - $result = $this->db->query( $sql, __METHOD__ ); - $resultset = $this->db->resultObject( $result ); - foreach ( $resultset as $row ) { + // rev_deleted + + $res = $this->db->select( + array( 'page', 'revision' ), + array( 'DISTINCT rev_user_text', 'rev_user' ), + array( + $this->db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0', + $cond, + 'page_id = rev_id', + ), + __METHOD__ + ); + + foreach ( $res as $row ) { $this->author_list .= "" . "" . htmlentities( $row->rev_user_text ) . @@ -184,27 +208,27 @@ class WikiExporter { protected function dumpFrom( $cond = '' ) { wfProfileIn( __METHOD__ ); # For logging dumps... - if( $this->history & self::LOGS ) { - if( $this->buffer == WikiExporter::STREAM ) { + if ( $this->history & self::LOGS ) { + if ( $this->buffer == WikiExporter::STREAM ) { $prev = $this->db->bufferResults( false ); } $where = array( 'user_id = log_user' ); # Hide private logs $hideLogs = LogEventsList::getExcludeClause( $this->db ); - if( $hideLogs ) $where[] = $hideLogs; + if ( $hideLogs ) $where[] = $hideLogs; # Add on any caller specified conditions - if( $cond ) $where[] = $cond; + if ( $cond ) $where[] = $cond; # Get logging table name for logging.* clause - $logging = $this->db->tableName('logging'); - $result = $this->db->select( array('logging','user'), + $logging = $this->db->tableName( 'logging' ); + $result = $this->db->select( array( 'logging', 'user' ), array( "{$logging}.*", 'user_name' ), // grab the user name $where, __METHOD__, - array( 'ORDER BY' => 'log_id', 'USE INDEX' => array('logging' => 'PRIMARY') ) + array( 'ORDER BY' => 'log_id', 'USE INDEX' => array( 'logging' => 'PRIMARY' ) ) ); $wrapper = $this->db->resultObject( $result ); $this->outputLogStream( $wrapper ); - if( $this->buffer == WikiExporter::STREAM ) { + if ( $this->buffer == WikiExporter::STREAM ) { $this->db->bufferResults( $prev ); } # For page dumps... @@ -213,11 +237,11 @@ class WikiExporter { $opts = array( 'ORDER BY' => 'page_id ASC' ); $opts['USE INDEX'] = array(); $join = array(); - if( is_array( $this->history ) ) { + if ( is_array( $this->history ) ) { # Time offset/limit for all pages/history... $revJoin = 'page_id=rev_page'; # Set time order - if( $this->history['dir'] == 'asc' ) { + if ( $this->history['dir'] == 'asc' ) { $op = '>'; $opts['ORDER BY'] = 'rev_timestamp ASC'; } else { @@ -225,54 +249,57 @@ class WikiExporter { $opts['ORDER BY'] = 'rev_timestamp DESC'; } # Set offset - if( !empty( $this->history['offset'] ) ) { + if ( !empty( $this->history['offset'] ) ) { $revJoin .= " AND rev_timestamp $op " . $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) ); } - $join['revision'] = array('INNER JOIN',$revJoin); + $join['revision'] = array( 'INNER JOIN', $revJoin ); # Set query limit - if( !empty( $this->history['limit'] ) ) { + if ( !empty( $this->history['limit'] ) ) { $opts['LIMIT'] = intval( $this->history['limit'] ); } - } elseif( $this->history & WikiExporter::FULL ) { + } elseif ( $this->history & WikiExporter::FULL ) { # Full history dumps... - $join['revision'] = array('INNER JOIN','page_id=rev_page'); - } elseif( $this->history & WikiExporter::CURRENT ) { + $join['revision'] = array( 'INNER JOIN', 'page_id=rev_page' ); + } elseif ( $this->history & WikiExporter::CURRENT ) { # Latest revision dumps... - if( $this->list_authors && $cond != '' ) { // List authors, if so desired - list($page,$revision) = $this->db->tableNamesN('page','revision'); - $this->do_list_authors( $page, $revision, $cond ); + if ( $this->list_authors && $cond != '' ) { // List authors, if so desired + $this->do_list_authors( $cond ); } - $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id'); - } elseif( $this->history & WikiExporter::STABLE ) { + $join['revision'] = array( 'INNER JOIN', 'page_id=rev_page AND page_latest=rev_id' ); + } elseif ( $this->history & WikiExporter::STABLE ) { # "Stable" revision dumps... # Default JOIN, to be overridden... - $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id'); + $join['revision'] = array( 'INNER JOIN', 'page_id=rev_page AND page_latest=rev_id' ); # One, and only one hook should set this, and return false - if( wfRunHooks( 'WikiExporter::dumpStableQuery', array(&$tables,&$opts,&$join) ) ) { + if ( wfRunHooks( 'WikiExporter::dumpStableQuery', array( &$tables, &$opts, &$join ) ) ) { wfProfileOut( __METHOD__ ); - throw new MWException( __METHOD__." given invalid history dump type." ); + throw new MWException( __METHOD__ . " given invalid history dump type." ); } + } elseif ( $this->history & WikiExporter::RANGE ) { + # Dump of revisions within a specified range + $join['revision'] = array( 'INNER JOIN', 'page_id=rev_page' ); + $opts['ORDER BY'] = 'rev_page ASC, rev_id ASC'; } else { # Uknown history specification parameter? wfProfileOut( __METHOD__ ); - throw new MWException( __METHOD__." given invalid history dump type." ); + throw new MWException( __METHOD__ . " given invalid history dump type." ); } # Query optimization hacks - if( $cond == '' ) { + if ( $cond == '' ) { $opts[] = 'STRAIGHT_JOIN'; $opts['USE INDEX']['page'] = 'PRIMARY'; } # Build text join options - if( $this->text != WikiExporter::STUB ) { // 1-pass + if ( $this->text != WikiExporter::STUB ) { // 1-pass $tables[] = 'text'; - $join['text'] = array('INNER JOIN','rev_text_id=old_id'); + $join['text'] = array( 'INNER JOIN', 'rev_text_id=old_id' ); } - if( $this->buffer == WikiExporter::STREAM ) { + if ( $this->buffer == WikiExporter::STREAM ) { $prev = $this->db->bufferResults( false ); } - + wfRunHooks( 'ModifyExportQuery', array( $this->db, &$tables, &$cond, &$opts, &$join ) ); @@ -281,11 +308,11 @@ class WikiExporter { $wrapper = $this->db->resultObject( $result ); # Output dump results $this->outputPageStream( $wrapper ); - if( $this->list_authors ) { + if ( $this->list_authors ) { $this->outputPageStream( $wrapper ); } - if( $this->buffer == WikiExporter::STREAM ) { + if ( $this->buffer == WikiExporter::STREAM ) { $this->db->bufferResults( $prev ); } } @@ -307,13 +334,13 @@ class WikiExporter { protected function outputPageStream( $resultset ) { $last = null; foreach ( $resultset as $row ) { - if( is_null( $last ) || + if ( is_null( $last ) || $last->page_namespace != $row->page_namespace || $last->page_title != $row->page_title ) { - if( isset( $last ) ) { + if ( isset( $last ) ) { $output = ''; - if( $this->dumpUploads ) { - $output .= $this->writer->writeUploads( $last ); + if ( $this->dumpUploads ) { + $output .= $this->writer->writeUploads( $last, $this->dumpUploadFileContents ); } $output .= $this->writer->closePage(); $this->sink->writeClosePage( $output ); @@ -325,17 +352,17 @@ class WikiExporter { $output = $this->writer->writeRevision( $row ); $this->sink->writeRevision( $row, $output ); } - if( isset( $last ) ) { + if ( isset( $last ) ) { $output = ''; - if( $this->dumpUploads ) { - $output .= $this->writer->writeUploads( $last ); + if ( $this->dumpUploads ) { + $output .= $this->writer->writeUploads( $last, $this->dumpUploadFileContents ); } $output .= $this->author_list; $output .= $this->writer->closePage(); $this->sink->writeClosePage( $output ); } } - + protected function outputLogStream( $resultset ) { foreach ( $resultset as $row ) { $output = $this->writer->writeLogItem( $row ); @@ -348,7 +375,6 @@ class WikiExporter { * @ingroup Dump */ class XmlDumpWriter { - /** * Returns the export schema version. * @return string @@ -405,7 +431,7 @@ class XmlDumpWriter { } function homelink() { - return Xml::element( 'base', array(), Title::newMainPage()->getFullUrl() ); + return Xml::element( 'base', array(), Title::newMainPage()->getCanonicalUrl() ); } function caseSetting() { @@ -418,9 +444,9 @@ class XmlDumpWriter { function namespaces() { global $wgContLang; $spaces = "\n"; - foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) { - $spaces .= ' ' . - Xml::element( 'namespace', + foreach ( $wgContLang->getFormattedNamespaces() as $ns => $title ) { + $spaces .= ' ' . + Xml::element( 'namespace', array( 'key' => $ns, 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive', ), $title ) . "\n"; @@ -432,12 +458,13 @@ class XmlDumpWriter { /** * Closes the output stream with the closing root element. * Call when finished dumping things. + * + * @return string */ function closeStream() { return "\n"; } - /** * Opens a section on the output stream, with data * from the given database row. @@ -449,18 +476,18 @@ class XmlDumpWriter { function openPage( $row ) { $out = " \n"; $title = Title::makeTitle( $row->page_namespace, $row->page_title ); - $out .= ' ' . Xml::elementClean( 'title', array(), $title->getPrefixedText() ) . "\n"; + $out .= ' ' . Xml::elementClean( 'title', array(), self::canonicalTitle( $title ) ) . "\n"; $out .= ' ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n"; - if( $row->page_is_redirect ) { + if ( $row->page_is_redirect ) { $out .= ' ' . Xml::element( 'redirect', array() ) . "\n"; } - if( $row->page_restrictions != '' ) { + if ( $row->page_restrictions != '' ) { $out .= ' ' . Xml::element( 'restrictions', array(), strval( $row->page_restrictions ) ) . "\n"; } - + wfRunHooks( 'XmlDumpWriterOpenPage', array( $this, &$out, $row, $title ) ); - + return $out; } @@ -489,25 +516,25 @@ class XmlDumpWriter { $out .= $this->writeTimestamp( $row->rev_timestamp ); - if( $row->rev_deleted & Revision::DELETED_USER ) { + if ( $row->rev_deleted & Revision::DELETED_USER ) { $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; } else { $out .= $this->writeContributor( $row->rev_user, $row->rev_user_text ); } - if( $row->rev_minor_edit ) { + if ( $row->rev_minor_edit ) { $out .= " \n"; } - if( $row->rev_deleted & Revision::DELETED_COMMENT ) { + if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; - } elseif( $row->rev_comment != '' ) { + } elseif ( $row->rev_comment != '' ) { $out .= " " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n"; } $text = ''; - if( $row->rev_deleted & Revision::DELETED_TEXT ) { + if ( $row->rev_deleted & Revision::DELETED_TEXT ) { $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; - } elseif( isset( $row->old_text ) ) { + } elseif ( isset( $row->old_text ) ) { // Raw text from the database may have invalid chars $text = strval( Revision::getRevisionText( $row ) ); $out .= " " . Xml::elementClean( 'text', @@ -527,7 +554,7 @@ class XmlDumpWriter { wfProfileOut( __METHOD__ ); return $out; } - + /** * Dumps a section on the output stream, with * data filled in from the given database row. @@ -544,26 +571,26 @@ class XmlDumpWriter { $out .= $this->writeTimestamp( $row->log_timestamp ); - if( $row->log_deleted & LogPage::DELETED_USER ) { + if ( $row->log_deleted & LogPage::DELETED_USER ) { $out .= " " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n"; } else { $out .= $this->writeContributor( $row->log_user, $row->user_name ); } - if( $row->log_deleted & LogPage::DELETED_COMMENT ) { + if ( $row->log_deleted & LogPage::DELETED_COMMENT ) { $out .= " " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n"; - } elseif( $row->log_comment != '' ) { + } elseif ( $row->log_comment != '' ) { $out .= " " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n"; } - + $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n"; $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n"; - if( $row->log_deleted & LogPage::DELETED_ACTION ) { + if ( $row->log_deleted & LogPage::DELETED_ACTION ) { $out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n"; } else { $title = Title::makeTitle( $row->log_namespace, $row->log_title ); - $out .= " " . Xml::elementClean( 'logtitle', null, $title->getPrefixedText() ) . "\n"; + $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n"; $out .= " " . Xml::elementClean( 'params', array( 'xml:space' => 'preserve' ), strval( $row->log_params ) ) . "\n"; @@ -582,7 +609,7 @@ class XmlDumpWriter { function writeContributor( $id, $text ) { $out = " \n"; - if( $id ) { + if ( $id ) { $out .= " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n"; $out .= " " . Xml::element( 'id', null, strval( $id ) ) . "\n"; } else { @@ -595,32 +622,79 @@ class XmlDumpWriter { /** * Warning! This data is potentially inconsistent. :( */ - function writeUploads( $row ) { - if( $row->page_namespace == NS_IMAGE ) { - $img = wfFindFile( $row->page_title ); - if( $img ) { + function writeUploads( $row, $dumpContents = false ) { + if ( $row->page_namespace == NS_IMAGE ) { + $img = wfLocalFile( $row->page_title ); + if ( $img && $img->exists() ) { $out = ''; - foreach( array_reverse( $img->getHistory() ) as $ver ) { - $out .= $this->writeUpload( $ver ); + foreach ( array_reverse( $img->getHistory() ) as $ver ) { + $out .= $this->writeUpload( $ver, $dumpContents ); } - $out .= $this->writeUpload( $img ); + $out .= $this->writeUpload( $img, $dumpContents ); return $out; } } return ''; } - function writeUpload( $file ) { + /** + * @param $file File + * @param $dumpContents bool + * @return string + */ + function writeUpload( $file, $dumpContents = false ) { + if ( $file->isOld() ) { + $archiveName = " " . + Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n"; + } else { + $archiveName = ''; + } + if ( $dumpContents ) { + # Dump file as base64 + # Uses only XML-safe characters, so does not need escaping + $contents = ' ' . + chunk_split( base64_encode( file_get_contents( $file->getPath() ) ) ) . + " \n"; + } else { + $contents = ''; + } return " \n" . $this->writeTimestamp( $file->getTimestamp() ) . $this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) . " " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" . " " . Xml::element( 'filename', null, $file->getName() ) . "\n" . - " " . Xml::element( 'src', null, $file->getFullUrl() ) . "\n" . + $archiveName . + " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" . " " . Xml::element( 'size', null, $file->getSize() ) . "\n" . + " " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" . + " " . Xml::element( 'rel', null, $file->getRel() ) . "\n" . + $contents . " \n"; } + /** + * Return prefixed text form of title, but using the content language's + * canonical namespace. This skips any special-casing such as gendered + * user namespaces -- which while useful, are not yet listed in the + * XML data so are unsafe in export. + * + * @param Title $title + * @return string + */ + public static function canonicalTitle( Title $title ) { + if ( $title->getInterwiki() ) { + return $title->getPrefixedText(); + } + + global $wgContLang; + $prefix = str_replace( '_', ' ', $wgContLang->getNsText( $title->getNamespace() ) ); + + if ($prefix !== '') { + $prefix .= ':'; + } + + return $prefix . $title->getText(); + } } @@ -648,7 +722,7 @@ class DumpOutput { function writeRevision( $rev, $string ) { $this->write( $string ); } - + function writeLogItem( $rev, $string ) { $this->write( $string ); } @@ -660,6 +734,36 @@ class DumpOutput { function write( $string ) { print $string; } + + /** + * Close the old file, move it to a specified name, + * and reopen new file with the old name. Use this + * for writing out a file in multiple pieces + * at specified checkpoints (e.g. every n hours). + * @param $newname mixed File name. May be a string or an array with one element + */ + function closeRenameAndReopen( $newname ) { + return; + } + + /** + * Close the old file, and move it to a specified name. + * Use this for the last piece of a file written out + * at specified checkpoints (e.g. every n hours). + * @param $newname mixed File name. May be a string or an array with one element + * @param $open bool If true, a new file with the old filename will be opened again for writing (default: false) + */ + function closeAndRename( $newname, $open = false ) { + return; + } + + /** + * Returns the name of the file or files which are + * being written to, if there are any. + */ + function getFilenames() { + return NULL; + } } /** @@ -667,15 +771,52 @@ class DumpOutput { * @ingroup Dump */ class DumpFileOutput extends DumpOutput { - var $handle; + protected $handle, $filename; function __construct( $file ) { $this->handle = fopen( $file, "wt" ); + $this->filename = $file; } function write( $string ) { fputs( $this->handle, $string ); } + + function closeRenameAndReopen( $newname ) { + $this->closeAndRename( $newname, true ); + } + + function renameOrException( $newname ) { + if (! rename( $this->filename, $newname ) ) { + throw new MWException( __METHOD__ . ": rename of file {$this->filename} to $newname failed\n" ); + } + } + + function checkRenameArgCount( $newname ) { + if ( is_array( $newname ) ) { + if ( count( $newname ) > 1 ) { + throw new MWException( __METHOD__ . ": passed multiple arguments for rename of single file\n" ); + } else { + $newname = $newname[0]; + } + } + return $newname; + } + + function closeAndRename( $newname, $open = false ) { + $newname = $this->checkRenameArgCount( $newname ); + if ( $newname ) { + fclose( $this->handle ); + $this->renameOrException( $newname ); + if ( $open ) { + $this->handle = fopen( $this->filename, "wt" ); + } + } + } + + function getFilenames() { + return $this->filename; + } } /** @@ -685,12 +826,45 @@ class DumpFileOutput extends DumpOutput { * @ingroup Dump */ class DumpPipeOutput extends DumpFileOutput { + protected $command, $filename; + function __construct( $command, $file = null ) { - if( !is_null( $file ) ) { + if ( !is_null( $file ) ) { $command .= " > " . wfEscapeShellArg( $file ); } - $this->handle = popen( $command, "w" ); + + $this->startCommand( $command ); + $this->command = $command; + $this->filename = $file; + } + + function startCommand( $command ) { + $spec = array( + 0 => array( "pipe", "r" ), + ); + $pipes = array(); + $this->procOpenResource = proc_open( $command, $spec, $pipes ); + $this->handle = $pipes[0]; } + + function closeRenameAndReopen( $newname ) { + $this->closeAndRename( $newname, true ); + } + + function closeAndRename( $newname, $open = false ) { + $newname = $this->checkRenameArgCount( $newname ); + if ( $newname ) { + fclose( $this->handle ); + proc_close( $this->procOpenResource ); + $this->renameOrException( $newname ); + if ( $open ) { + $command = $this->command; + $command .= " > " . wfEscapeShellArg( $this->filename ); + $this->startCommand( $command ); + } + } + } + } /** @@ -718,12 +892,37 @@ class DumpBZip2Output extends DumpPipeOutput { * @ingroup Dump */ class Dump7ZipOutput extends DumpPipeOutput { + protected $filename; + function __construct( $file ) { + $command = $this->setup7zCommand( $file ); + parent::__construct( $command ); + $this->filename = $file; + } + + function setup7zCommand( $file ) { $command = "7za a -bd -si " . wfEscapeShellArg( $file ); // Suppress annoying useless crap from p7zip // Unfortunately this could suppress real error messages too $command .= ' >' . wfGetNull() . ' 2>&1'; - parent::__construct( $command ); + return( $command ); + } + + function closeRenameAndReopen( $newname ) { + $this->closeAndRename( $newname, true ); + } + + function closeAndRename( $newname, $open = false ) { + $newname = $this->checkRenameArgCount( $newname ); + if ( $newname ) { + fclose( $this->handle ); + proc_close( $this->procOpenResource ); + $this->renameOrException( $newname ); + if ( $open ) { + $command = $this->setup7zCommand( $file ); + $this->startCommand( $command ); + } + } } } @@ -750,27 +949,39 @@ class DumpFilter { function writeOpenPage( $page, $string ) { $this->sendingThisPage = $this->pass( $page, $string ); - if( $this->sendingThisPage ) { + if ( $this->sendingThisPage ) { $this->sink->writeOpenPage( $page, $string ); } } function writeClosePage( $string ) { - if( $this->sendingThisPage ) { + if ( $this->sendingThisPage ) { $this->sink->writeClosePage( $string ); $this->sendingThisPage = false; } } function writeRevision( $rev, $string ) { - if( $this->sendingThisPage ) { + if ( $this->sendingThisPage ) { $this->sink->writeRevision( $rev, $string ); } } - + function writeLogItem( $rev, $string ) { $this->sink->writeRevision( $rev, $string ); - } + } + + function closeRenameAndReopen( $newname ) { + $this->sink->closeRenameAndReopen( $newname ); + } + + function closeAndRename( $newname, $open = false ) { + $this->sink->closeAndRename( $newname, $open ); + } + + function getFilenames() { + return $this->sink->getFilenames(); + } /** * Override for page-based filter types. @@ -822,17 +1033,17 @@ class DumpNamespaceFilter extends DumpFilter { "NS_CATEGORY" => NS_CATEGORY, "NS_CATEGORY_TALK" => NS_CATEGORY_TALK ); - if( $param{0} == '!' ) { + if ( $param { 0 } == '!' ) { $this->invert = true; $param = substr( $param, 1 ); } - foreach( explode( ',', $param ) as $key ) { + foreach ( explode( ',', $param ) as $key ) { $key = trim( $key ); - if( isset( $constants[$key] ) ) { + if ( isset( $constants[$key] ) ) { $ns = $constants[$key]; $this->namespaces[$ns] = true; - } elseif( is_numeric( $key ) ) { + } elseif ( is_numeric( $key ) ) { $ns = intval( $key ); $this->namespaces[$ns] = true; } else { @@ -861,7 +1072,7 @@ class DumpLatestFilter extends DumpFilter { } function writeClosePage( $string ) { - if( $this->rev ) { + if ( $this->rev ) { $this->sink->writeOpenPage( $this->page, $this->pageString ); $this->sink->writeRevision( $this->rev, $this->revString ); $this->sink->writeClosePage( $string ); @@ -873,7 +1084,7 @@ class DumpLatestFilter extends DumpFilter { } function writeRevision( $rev, $string ) { - if( $rev->rev_id == $this->page->page_latest ) { + if ( $rev->rev_id == $this->page->page_latest ) { $this->rev = $rev; $this->revString = $string; } @@ -891,34 +1102,53 @@ class DumpMultiWriter { } function writeOpenStream( $string ) { - for( $i = 0; $i < $this->count; $i++ ) { + for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeOpenStream( $string ); } } function writeCloseStream( $string ) { - for( $i = 0; $i < $this->count; $i++ ) { + for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeCloseStream( $string ); } } function writeOpenPage( $page, $string ) { - for( $i = 0; $i < $this->count; $i++ ) { + for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeOpenPage( $page, $string ); } } function writeClosePage( $string ) { - for( $i = 0; $i < $this->count; $i++ ) { + for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeClosePage( $string ); } } function writeRevision( $rev, $string ) { - for( $i = 0; $i < $this->count; $i++ ) { + for ( $i = 0; $i < $this->count; $i++ ) { $this->sinks[$i]->writeRevision( $rev, $string ); } } + + function closeRenameAndReopen( $newnames ) { + $this->closeAndRename( $newnames, true ); + } + + function closeAndRename( $newnames, $open = false ) { + for ( $i = 0; $i < $this->count; $i++ ) { + $this->sinks[$i]->closeAndRename( $newnames[$i], $open ); + } + } + + function getFilenames() { + $filenames = array(); + for ( $i = 0; $i < $this->count; $i++ ) { + $filenames[] = $this->sinks[$i]->getFilenames(); + } + return $filenames; + } + } function xmlsafe( $string ) { diff --git a/includes/ExternalEdit.php b/includes/ExternalEdit.php index 7109c1ac..bf97c1a5 100644 --- a/includes/ExternalEdit.php +++ b/includes/ExternalEdit.php @@ -19,39 +19,54 @@ * */ class ExternalEdit { + /** + * Title to perform the edit on + * @var Title + */ + private $title; - function __construct( $article, $mode ) { - global $wgInputEncoding; - $this->mArticle =& $article; - $this->mTitle =& $article->mTitle; - $this->mCharset = $wgInputEncoding; - $this->mMode = $mode; + /** + * Mode of editing + * @var String + */ + private $mode; + + /** + * Constructor + * @param $title Title object we're performing the edit on + * @param $mode String What mode we're using. Only 'file' has any effect + */ + public function __construct( $title, $mode ) { + $this->title = $title; + $this->mode = $mode; } - function edit() { - global $wgOut, $wgScript, $wgScriptPath, $wgServer, $wgLang; + /** + * Output the information for the external editor + */ + public function edit() { + global $wgOut, $wgScript, $wgScriptPath, $wgCanonicalServer, $wgLang; $wgOut->disable(); - $name=$this->mTitle->getText(); - $pos=strrpos($name,".")+1; - header ( "Content-type: application/x-external-editor; charset=".$this->mCharset ); - header( "Cache-control: no-cache" ); + header( 'Content-type: application/x-external-editor; charset=utf-8' ); + header( 'Cache-control: no-cache' ); # $type can be "Edit text", "Edit file" or "Diff text" at the moment # See the protocol specifications at [[m:Help:External editors/Tech]] for # details. - if(!isset($this->mMode)) { - $type="Edit text"; - $url=$this->mTitle->getFullURL("action=edit&internaledit=true"); + if( $this->mode == "file" ) { + $type = "Edit file"; + $image = wfLocalFile( $this->title ); + $url = $image->getCanonicalURL(); + $extension = $image->getExtension(); + } else { + $type = "Edit text"; + $url = $this->title->getCanonicalURL( + array( 'action' => 'edit', 'internaledit' => 'true' ) ); # *.wiki file extension is used by some editors for syntax # highlighting, so we follow that convention - $extension="wiki"; - } elseif($this->mMode=="file") { - $type="Edit file"; - $image = wfLocalFile( $this->mTitle ); - $url = $image->getFullURL(); - $extension=substr($name, $pos); + $extension = "wiki"; } - $special=$wgLang->getNsText(NS_SPECIAL); + $special = $wgLang->getNsText( NS_SPECIAL ); $control = << $wiki ) ); } diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php index 877277a2..552c3109 100644 --- a/includes/ExternalStoreDB.php +++ b/includes/ExternalStoreDB.php @@ -18,7 +18,7 @@ class ExternalStoreDB { */ function &getLoadBalancer( $cluster ) { $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false; - + return wfGetLBFactory()->getExternalLB( $cluster, $wiki ); } @@ -29,8 +29,18 @@ class ExternalStoreDB { * @return DatabaseBase object */ function &getSlave( $cluster ) { + global $wgDefaultExternalStore; + $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false; $lb =& $this->getLoadBalancer( $cluster ); + + if ( !in_array( "DB://" . $cluster, $wgDefaultExternalStore ) ) { + wfDebug( "read only external store" ); + $lb->allowLagged(true); + } else { + wfDebug( "writable external store" ); + } + return $lb->getConnection( DB_SLAVE, array(), $wiki ); } @@ -139,8 +149,8 @@ class ExternalStoreDB { function store( $cluster, $data ) { $dbw = $this->getMaster( $cluster ); $id = $dbw->nextSequenceValue( 'blob_blob_id_seq' ); - $dbw->insert( $this->getTable( $dbw ), - array( 'blob_id' => $id, 'blob_text' => $data ), + $dbw->insert( $this->getTable( $dbw ), + array( 'blob_id' => $id, 'blob_text' => $data ), __METHOD__ ); $id = $dbw->insertId(); if ( !$id ) { diff --git a/includes/ExternalUser.php b/includes/ExternalUser.php index d1eff916..37716390 100644 --- a/includes/ExternalUser.php +++ b/includes/ExternalUser.php @@ -98,7 +98,7 @@ abstract class ExternalUser { * This is a wrapper around newFromId(). * * @param $user User - * @return mixed ExternalUser or false + * @return ExternalUser|false */ public static function newFromUser( $user ) { global $wgExternalAuthType; @@ -293,7 +293,7 @@ abstract class ExternalUser { * @return Mixed User if the account is linked, Null otherwise. */ public final function getLocalUser(){ - $dbr = wfGetDb( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'external_user', '*', diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php index 21b49bde..515ff387 100644 --- a/includes/FakeTitle.php +++ b/includes/FakeTitle.php @@ -6,10 +6,6 @@ class FakeTitle extends Title { function error() { throw new MWException( "Attempt to call member function of FakeTitle\n" ); } - // PHP 5.1 method overload - function __call( $name, $args ) { $this->error(); } - - // PHP <5.1 compatibility function isLocal() { $this->error(); } function isTrans() { $this->error(); } function getText() { $this->error(); } @@ -33,7 +29,7 @@ class FakeTitle extends Title { function getSubpageText() { $this->error(); } function getSubpageUrlForm() { $this->error(); } function getPrefixedURL() { $this->error(); } - function getFullURL( $query = '', $variant = false ) {$this->error(); } + function getFullURL( $query = '', $variant = false ) { $this->error(); } function getLocalURL( $query = '', $variant = false ) { $this->error(); } function getLinkUrl( $query = array(), $variant = false ) { $this->error(); } function escapeLocalURL( $query = '' ) { $this->error(); } diff --git a/includes/Fallback.php b/includes/Fallback.php new file mode 100644 index 00000000..2cca1e81 --- /dev/null +++ b/includes/Fallback.php @@ -0,0 +1,200 @@ + 0 ) { + if( $splitPos > 256 ) { + // Optimize large string offsets by skipping ahead N bytes. + // This will cut out most of our slow time on Latin-based text, + // and 1/2 to 1/3 on East European and Asian scripts. + $bytePos = $splitPos; + while ( $bytePos < $byteLen && $str[$bytePos] >= "\x80" && $str[$bytePos] < "\xc0" ) { + ++$bytePos; + } + $charPos = mb_strlen( substr( $str, 0, $bytePos ) ); + } else { + $charPos = 0; + $bytePos = 0; + } + + while( $charPos++ < $splitPos ) { + ++$bytePos; + // Move past any tail bytes + while ( $bytePos < $byteLen && $str[$bytePos] >= "\x80" && $str[$bytePos] < "\xc0" ) { + ++$bytePos; + } + } + } else { + $splitPosX = $splitPos + 1; + $charPos = 0; // relative to end of string; we don't care about the actual char position here + $bytePos = $byteLen; + while( $bytePos > 0 && $charPos-- >= $splitPosX ) { + --$bytePos; + // Move past any tail bytes + while ( $bytePos > 0 && $str[$bytePos] >= "\x80" && $str[$bytePos] < "\xc0" ) { + --$bytePos; + } + } + } + + return $bytePos; + } + + /** + * Fallback implementation of mb_strlen, hardcoded to UTF-8. + * @param string $str + * @param string $enc optional encoding; ignored + * @return int + */ + public static function mb_strlen( $str, $enc = '' ) { + $counts = count_chars( $str ); + $total = 0; + + // Count ASCII bytes + for( $i = 0; $i < 0x80; $i++ ) { + $total += $counts[$i]; + } + + // Count multibyte sequence heads + for( $i = 0xc0; $i < 0xff; $i++ ) { + $total += $counts[$i]; + } + return $total; + } + + + /** + * Fallback implementation of mb_strpos, hardcoded to UTF-8. + * @param $haystack String + * @param $needle String + * @param $offset String: optional start position + * @param $encoding String: optional encoding; ignored + * @return int + */ + public static function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { + $needle = preg_quote( $needle, '/' ); + + $ar = array(); + preg_match( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset ); + + if( isset( $ar[0][1] ) ) { + return $ar[0][1]; + } else { + return false; + } + } + + /** + * Fallback implementation of mb_strrpos, hardcoded to UTF-8. + * @param $haystack String + * @param $needle String + * @param $offset String: optional start position + * @param $encoding String: optional encoding; ignored + * @return int + */ + public static function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { + $needle = preg_quote( $needle, '/' ); + + $ar = array(); + preg_match_all( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset ); + + if( isset( $ar[0] ) && count( $ar[0] ) > 0 && + isset( $ar[0][count( $ar[0] ) - 1][1] ) ) { + return $ar[0][count( $ar[0] ) - 1][1]; + } else { + return false; + } + } + + /** + * Fallback implementation of stream_resolve_include_path() + * Native stream_resolve_include_path is available for PHP 5 >= 5.3.2 + * @param $filename String + * @return String + */ + public static function stream_resolve_include_path( $filename ) { + $pathArray = explode( PATH_SEPARATOR, get_include_path() ); + foreach ( $pathArray as $path ) { + $fullFilename = $path . DIRECTORY_SEPARATOR . $filename; + if ( file_exists( $fullFilename ) ) { + return $fullFilename; + } + } + return false; + } + +} diff --git a/includes/Feed.php b/includes/Feed.php index bc20a9dc..ef33c78f 100644 --- a/includes/Feed.php +++ b/includes/Feed.php @@ -36,9 +36,8 @@ * @ingroup Feed */ class FeedItem { - /**#@+ - * @var string - * @private + /** + * @var Title */ var $Title = 'Wiki'; var $Description = ''; @@ -47,12 +46,11 @@ class FeedItem { var $Author = ''; var $UniqueId = ''; var $RSSIsPermalink; - /**#@-*/ /** * Constructor * - * @param $Title String: Item's title + * @param $Title String|Title Item's title * @param $Description String * @param $Url String: URL uniquely designating the item. * @param $Date String: Item's date @@ -70,6 +68,15 @@ class FeedItem { $this->Comments = $Comments; } + /** + * Get the last touched timestamp + * + * @return String last-touched timestamp + */ + public function getLastMod() { + return $this->Title->getTouched(); + } + /** * Encode $string so that it can be safely embedded in a XML document * @@ -99,7 +106,7 @@ class FeedItem { * @param $uniqueId String: unique id for the item * @param $RSSisPermalink Boolean: set to true if the guid (unique id) is a permalink (RSS feeds only) */ - public function setUniqueId($uniqueId, $RSSisPermalink = False) { + public function setUniqueId($uniqueId, $RSSisPermalink = false) { $this->UniqueId = $uniqueId; $this->RSSIsPermalink = $RSSisPermalink; } @@ -113,6 +120,16 @@ class FeedItem { return $this->xmlEncode( $this->Title ); } + /** + * Get the DB prefixed title + * + * @return String the prefixed title, with underscores and + * any interwiki and namespace prefixes + */ + public function getDBPrefixedTitle() { + return $this->Title->getPrefixedDBKey(); + } + /** * Get the URL of this item; already xml-encoded * @@ -222,12 +239,15 @@ class ChannelFeed extends FeedItem { * but can also be called separately. */ public function httpHeaders() { - global $wgOut; + global $wgOut, $wgVaryOnXFP; # We take over from $wgOut, excepting its cache header info $wgOut->disable(); $mimetype = $this->contentType(); header( "Content-type: $mimetype; charset=UTF-8" ); + if ( $wgVaryOnXFP ) { + $wgOut->addVaryHeader( 'X-Forwarded-Proto' ); + } $wgOut->sendCacheControl(); } @@ -256,7 +276,7 @@ class ChannelFeed extends FeedItem { $this->httpHeaders(); echo '' . "\n"; echo '\n"; } } @@ -288,7 +308,7 @@ class RSSFeed extends ChannelFeed { ?> <?php print $this->getTitle() ?> - getUrl() ?> + getUrl(), PROTO_CURRENT ) ?> getDescription() ?> getLanguage() ?> MediaWiki @@ -304,12 +324,12 @@ class RSSFeed extends ChannelFeed { ?> <?php print $item->getTitle() ?> - getUrl() ?> + getUrl(), PROTO_CURRENT ) ?> RSSIsPermalink ) print ' isPermaLink="false"' ?>>getUniqueId() ?> getDescription() ?> getDate() ) { ?>formatTime( $item->getDate() ) ?> getAuthor() ) { ?>getAuthor() ?> - getComments() ) { ?>getComments() ?> + getComments() ) { ?>getComments(), PROTO_CURRENT ) ?> getFeedId() ?> <?php print $this->getTitle() ?> - - + + formatTime( wfTimestampNow() ) ?>Z getDescription() ?> MediaWiki @@ -390,7 +410,7 @@ class AtomFeed extends ChannelFeed { getUniqueId() ?> <?php print $item->getTitle() ?> - + getDate() ) { ?> formatTime( $item->getDate() ) ?>Z @@ -399,9 +419,9 @@ class AtomFeed extends ChannelFeed { getAuthor() ) { ?>getAuthor() ?> -getComments() ) { ?>getComments() ?> - */ + */ } /** diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php index 9daffc12..4502c3a8 100644 --- a/includes/FeedUtils.php +++ b/includes/FeedUtils.php @@ -112,7 +112,7 @@ class FeedUtils { # $wgLang->date( $timestamp ), # $wgLang->time( $timestamp ) ), # wfMsg( 'currentrev' ) ); - + // Don't bother generating the diff if we won't be able to show it if ( $wgFeedDiffCutoff > 0 ) { $de = new DifferenceEngine( $title, $oldid, $newid ); diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 030330bb..515768ff 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -118,25 +118,21 @@ class FileDeleteForm { } else { $id = $title->getArticleID( Title::GAID_FOR_UPDATE ); $article = new Article( $title ); - $error = ''; $dbw = wfGetDB( DB_MASTER ); try { - if( wfRunHooks( 'ArticleDelete', array( &$article, &$wgUser, &$reason, &$error ) ) ) { - // delete the associated article first - if( $article->doDeleteArticle( $reason, $suppress, $id, false ) ) { - global $wgRequest; - if( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { - $article->doWatch(); - } elseif( $title->userIsWatching() ) { - $article->doUnwatch(); - } - $status = $file->delete( $reason, $suppress ); - if( $status->ok ) { - $dbw->commit(); - wfRunHooks( 'ArticleDeleteComplete', array( &$article, &$wgUser, $reason, $id ) ); - } else { - $dbw->rollback(); - } + // delete the associated article first + if( $article->doDeleteArticle( $reason, $suppress, $id, false ) ) { + global $wgRequest; + if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) { + WatchAction::doWatch( $title, $wgUser ); + } elseif ( $title->userIsWatching() ) { + WatchAction::doUnwatch( $title, $wgUser ); + } + $status = $file->delete( $reason, $suppress ); + if( $status->ok ) { + $dbw->commit(); + } else { + $dbw->rollback(); } } } catch ( MWException $e ) { @@ -257,15 +253,15 @@ class FileDeleteForm { return wfMsgExt( "{$message}-old", # To ensure grep will find them: 'filedelete-intro-old', 'filedelete-nofile-old', 'filedelete-success-old' 'parse', - $this->title->getText(), + wfEscapeWikiText( $this->title->getText() ), $wgLang->date( $this->getTimestamp(), true ), $wgLang->time( $this->getTimestamp(), true ), - wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ) ) ); + wfExpandUrl( $this->file->getArchiveUrl( $this->oldimage ), PROTO_CURRENT ) ); } else { return wfMsgExt( $message, 'parse', - $this->title->getText() + wfEscapeWikiText( $this->title->getText() ) ); } } diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php deleted file mode 100644 index 47084aad..00000000 --- a/includes/FileRevertForm.php +++ /dev/null @@ -1,180 +0,0 @@ - - */ -class FileRevertForm { - - protected $title = null; - protected $file = null; - protected $archiveName = ''; - protected $timestamp = false; - protected $oldFile; - - /** - * Constructor - * - * @param $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; - $this->setHeaders(); - - if( wfReadOnly() ) { - $wgOut->readOnlyPage(); - return; - } elseif( !$wgUser->isLoggedIn() ) { - $wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' ); - return; - } elseif( !$this->title->userCan( 'edit' ) || !$this->title->userCan( 'upload' ) ) { - // 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->archiveName = $wgRequest->getText( 'oldimage' ); - $token = $wgRequest->getText( 'wpEditToken' ); - if( !$this->isValidOldSpec() ) { - $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->archiveName ) ); - 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->archiveName ) ) { - $source = $this->file->getArchiveVirtualUrl( $this->archiveName ); - $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 ), - wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ) ); - $wgOut->returnToMain( false, $this->title ); - } else { - $wgOut->addWikiText( $status->getWikiText() ); - } - return; - } - - // Show the form - $this->showForm(); - } - - /** - * Show the confirmation form - */ - protected function showForm() { - global $wgOut, $wgUser, $wgLang, $wgContLang; - $timestamp = $this->getTimestamp(); - - $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ); - $form .= Html::hidden( 'wpEditToken', $wgUser->editToken( $this->archiveName ) ); - $form .= '
    ' . wfMsgHtml( 'filerevert-legend' ) . ''; - $form .= wfMsgExt( 'filerevert-intro', 'parse', $this->title->getText(), - $wgLang->date( $timestamp, true ), $wgLang->time( $timestamp, true ), - wfExpandUrl( $this->file->getArchiveUrl( $this->archiveName ) ) ); - $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 - */ - protected function setHeaders() { - global $wgOut, $wgUser; - $wgOut->setPageTitle( wfMsg( 'filerevert', $this->title->getText() ) ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); - $wgOut->setSubtitle( wfMsg( - 'filerevert-backlink', - $wgUser->getSkin()->link( - $this->title, - null, - array(), - array(), - array( 'known', 'noclasses' ) - ) - ) ); - } - - /** - * Is the provided `oldimage` value valid? - * - * @return bool - */ - protected function isValidOldSpec() { - return strlen( $this->archiveName ) >= 16 - && strpos( $this->archiveName, '/' ) === false - && strpos( $this->archiveName, '\\' ) === false; - } - - /** - * Does the provided `oldimage` value correspond - * to an existing, local, old version of this file? - * - * @return bool - */ - protected function haveOldVersion() { - return $this->getOldFile()->exists(); - } - - /** - * Prepare the form action - * - * @return string - */ - protected function getAction() { - $q = array(); - $q[] = 'action=revert'; - $q[] = 'oldimage=' . urlencode( $this->archiveName ); - return $this->title->getLocalUrl( implode( '&', $q ) ); - } - - /** - * Extract the timestamp of the old version - * - * @return string - */ - protected function getTimestamp() { - if( $this->timestamp === false ) { - $this->timestamp = $this->getOldFile()->getTimestamp(); - } - return $this->timestamp; - } - - protected function getOldFile() { - if ( !isset( $this->oldFile ) ) { - $this->oldFile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->title, $this->archiveName ); - } - return $this->oldFile; - } -} diff --git a/includes/ForkController.php b/includes/ForkController.php index e5b44c2b..d87dfb1e 100644 --- a/includes/ForkController.php +++ b/includes/ForkController.php @@ -115,15 +115,17 @@ class ForkController { } protected function prepareEnvironment() { - global $wgCaches, $wgMemc; + global $wgMemc; // Don't share DB or memcached connections wfGetLBFactory()->destroyInstance(); - $wgCaches = array(); + ObjectCache::clear(); unset( $wgMemc ); } /** * Fork a number of worker processes. + * + * return string */ protected function forkWorkers( $numProcs ) { $this->prepareEnvironment(); diff --git a/includes/FormOptions.php b/includes/FormOptions.php index 2442a330..b668ff46 100644 --- a/includes/FormOptions.php +++ b/includes/FormOptions.php @@ -1,19 +1,38 @@ options[$name] ); } + /** + * Used to find out which type the data is. + * All types are defined in the 'Type constants' section of this class + * Please note we do not support detection of INTNULL MediaWiki type + * which will be assumed as INT if the data is an integer. + * + * @param $data Mixed: value to guess type for + * @exception MWException Unsupported datatype + * @return Type constant + */ public static function guessType( $data ) { if ( is_bool( $data ) ) { return self::BOOL; @@ -52,6 +81,13 @@ class FormOptions implements ArrayAccess { # Handling values + /** + * Verify the given option name exist. + * + * @param $name String: option name + * @param $strict Boolean: throw an exception when the option does not exist (default false) + * @return Boolean: true if option exist, false otherwise + */ public function validateName( $name, $strict = false ) { if ( !isset( $this->options[$name] ) ) { if ( $strict ) { @@ -63,6 +99,14 @@ class FormOptions implements ArrayAccess { return true; } + /** + * Use to set the value of an option. + * + * @param $name String: option name + * @param $value Mixed: value for the option + * @param $force Boolean: whether to set the value when it is equivalent to the default value for this option (default false). + * @return null + */ public function setValue( $name, $value, $force = false ) { $this->validateName( $name, true ); @@ -74,12 +118,24 @@ class FormOptions implements ArrayAccess { } } + /** + * Get the value for the given option name. + * Internally use getValueReal() + * + * @param $name String: option name + * @return Mixed + */ public function getValue( $name ) { $this->validateName( $name, true ); return $this->getValueReal( $this->options[$name] ); } + /** + * @todo Document + * @param $option Array: array structure describing the option + * @return Mixed. Value or the default value if it is null + */ protected function getValueReal( $option ) { if ( $option['value'] !== null ) { return $option['value']; @@ -88,11 +144,22 @@ class FormOptions implements ArrayAccess { } } + /** + * Delete the option value. + * This will make future calls to getValue() return the default value. + * @param $name String: option name + * @return null + */ public function reset( $name ) { $this->validateName( $name, true ); $this->options[$name]['value'] = null; } + /** + * @todo Document + * @param $name String: option name + * @return null + */ public function consumeValue( $name ) { $this->validateName( $name, true ); $this->options[$name]['consumed'] = true; @@ -100,6 +167,11 @@ class FormOptions implements ArrayAccess { return $this->getValueReal( $this->options[$name] ); } + /** + * @todo Document + * @param $names Array: array of option names + * @return null + */ public function consumeValues( /*Array*/ $names ) { $out = array(); @@ -112,8 +184,16 @@ class FormOptions implements ArrayAccess { return $out; } - # Validating values - + /** + * Validate and set an option integer value + * The value will be altered to fit in the range. + * + * @param $name String: option name + * @param $min Int: minimum value + * @param $max Int: maximum value + * @exception MWException Option is not of type int + * @return null + */ public function validateIntBounds( $name, $min, $max ) { $this->validateName( $name, true ); @@ -127,8 +207,11 @@ class FormOptions implements ArrayAccess { $this->setValue( $name, $value ); } - # Getting the data out for use - + /** + * Getting the data out for use + * @param $all Boolean: whether to include unchanged options (default: false) + * @return Array + */ public function getUnconsumedValues( $all = false ) { $values = array(); @@ -143,6 +226,10 @@ class FormOptions implements ArrayAccess { return $values; } + /** + * Return options modified as an array ( name => value ) + * @return Array + */ public function getChangedValues() { $values = array(); @@ -155,6 +242,10 @@ class FormOptions implements ArrayAccess { return $values; } + /** + * Format options to an array ( name => value) + * @return Array + */ public function getAllValues() { $values = array(); @@ -195,20 +286,26 @@ class FormOptions implements ArrayAccess { } } - /* ArrayAccess methods */ + /** @name ArrayAccess functions + * Those function implements PHP ArrayAccess interface + * @see http://php.net/manual/en/class.arrayaccess.php + */ + /* @{ */ + /** Whether option exist*/ public function offsetExists( $name ) { return isset( $this->options[$name] ); } - + /** Retrieve an option value */ public function offsetGet( $name ) { return $this->getValue( $name ); } - + /** Set an option to given value */ public function offsetSet( $name, $value ) { $this->setValue( $name, $value ); } - + /** Delete the option */ public function offsetUnset( $name ) { $this->delete( $name ); } + /* @} */ } diff --git a/includes/GenderCache.php b/includes/GenderCache.php new file mode 100644 index 00000000..a17ac024 --- /dev/null +++ b/includes/GenderCache.php @@ -0,0 +1,135 @@ +default === null ) { + $this->default = User::getDefaultOption( 'gender' ); + } + return $this->default; + } + + /** + * Returns the gender for given username. + * @param $username String: username + * @param $caller String: the calling method + * @return String + */ + public function getGenderOf( $username, $caller = '' ) { + global $wgUser; + + $username = strtr( $username, '_', ' ' ); + if ( !isset( $this->cache[$username] ) ) { + + if ( $this->misses >= $this->missLimit && $wgUser->getName() !== $username ) { + if( $this->misses === $this->missLimit ) { + $this->misses++; + wfDebug( __METHOD__ . ": too many misses, returning default onwards\n" ); + } + return $this->getDefault(); + + } else { + $this->misses++; + if ( !User::isValidUserName( $username ) ) { + $this->cache[$username] = $this->getDefault(); + } else { + $this->doQuery( $username, $caller ); + } + } + + } + + /* Undefined if there is a valid username which for some reason doesn't + * exist in the database. + */ + return isset( $this->cache[$username] ) ? $this->cache[$username] : $this->getDefault(); + } + + /** + * Wrapper for doQuery that processes raw LinkBatch data. + * + * @param $data + * @param $caller + */ + public function doLinkBatch( $data, $caller = '' ) { + $users = array(); + foreach ( $data as $ns => $pagenames ) { + if ( !MWNamespace::hasGenderDistinction( $ns ) ) continue; + foreach ( array_keys( $pagenames ) as $username ) { + if ( isset( $this->cache[$username] ) ) continue; + $users[$username] = true; + } + } + + $this->doQuery( array_keys( $users ), $caller ); + } + + /** + * Preloads genders for given list of users. + * @param $users List|String: usernames + * @param $caller String: the calling method + */ + public function doQuery( $users, $caller = '' ) { + $default = $this->getDefault(); + + foreach ( (array) $users as $index => $value ) { + $name = strtr( $value, '_', ' ' ); + if ( isset( $this->cache[$name] ) ) { + // Skip users whose gender setting we already know + unset( $users[$index] ); + } else { + $users[$index] = $name; + // For existing users, this value will be overwritten by the correct value + $this->cache[$name] = $default; + } + } + + if ( count( $users ) === 0 ) { + return false; + } + + $dbr = wfGetDB( DB_SLAVE ); + $table = array( 'user', 'user_properties' ); + $fields = array( 'user_name', 'up_value' ); + $conds = array( 'user_name' => $users ); + $joins = array( 'user_properties' => + array( 'LEFT JOIN', array( 'user_id = up_user', 'up_property' => 'gender' ) ) ); + + $comment = __METHOD__; + if ( strval( $caller ) !== '' ) { + $comment .= "/$caller"; + } + $res = $dbr->select( $table, $fields, $conds, $comment, $joins, $joins ); + + foreach ( $res as $row ) { + $this->cache[$row->user_name] = $row->up_value ? $row->up_value : $default; + } + } + +} diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 2c35568b..3424211f 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -8,188 +8,68 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point" ); } -require_once dirname( __FILE__ ) . '/normal/UtfNormalUtil.php'; - // Hide compatibility functions from Doxygen /// @cond /** * Compatibility functions * - * We support PHP 5.1.x and up. + * We support PHP 5.2.3 and up. * Re-implementations of newer functions or functions in non-standard * PHP extensions may be included here. */ + if( !function_exists( 'iconv' ) ) { - # iconv support is not in the default configuration and so may not be present. - # Assume will only ever use utf-8 and iso-8859-1. - # This will *not* work in all circumstances. + /** @codeCoverageIgnore */ function iconv( $from, $to, $string ) { - if ( substr( $to, -8 ) == '//IGNORE' ) { - $to = substr( $to, 0, strlen( $to ) - 8 ); - } - if( strcasecmp( $from, $to ) == 0 ) { - return $string; - } - if( strcasecmp( $from, 'utf-8' ) == 0 ) { - return utf8_decode( $string ); - } - if( strcasecmp( $to, 'utf-8' ) == 0 ) { - return utf8_encode( $string ); - } - return $string; + return Fallback::iconv( $from, $to, $string ); } } if ( !function_exists( 'mb_substr' ) ) { - /** - * Fallback implementation for mb_substr, hardcoded to UTF-8. - * Attempts to be at least _moderately_ efficient; best optimized - * for relatively small offset and count values -- about 5x slower - * than native mb_string in my testing. - * - * Larger offsets are still fairly efficient for Latin text, but - * can be up to 100x slower than native if the text is heavily - * multibyte and we have to slog through a few hundred kb. - */ + /** @codeCoverageIgnore */ function mb_substr( $str, $start, $count='end' ) { - if( $start != 0 ) { - $split = mb_substr_split_unicode( $str, intval( $start ) ); - $str = substr( $str, $split ); - } - - if( $count !== 'end' ) { - $split = mb_substr_split_unicode( $str, intval( $count ) ); - $str = substr( $str, 0, $split ); - } - - return $str; + return Fallback::mb_substr( $str, $start, $count ); } + /** @codeCoverageIgnore */ function mb_substr_split_unicode( $str, $splitPos ) { - if( $splitPos == 0 ) { - return 0; - } - - $byteLen = strlen( $str ); - - if( $splitPos > 0 ) { - if( $splitPos > 256 ) { - // Optimize large string offsets by skipping ahead N bytes. - // This will cut out most of our slow time on Latin-based text, - // and 1/2 to 1/3 on East European and Asian scripts. - $bytePos = $splitPos; - while ( $bytePos < $byteLen && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0" ) { - ++$bytePos; - } - $charPos = mb_strlen( substr( $str, 0, $bytePos ) ); - } else { - $charPos = 0; - $bytePos = 0; - } - - while( $charPos++ < $splitPos ) { - ++$bytePos; - // Move past any tail bytes - while ( $bytePos < $byteLen && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0" ) { - ++$bytePos; - } - } - } else { - $splitPosX = $splitPos + 1; - $charPos = 0; // relative to end of string; we don't care about the actual char position here - $bytePos = $byteLen; - while( $bytePos > 0 && $charPos-- >= $splitPosX ) { - --$bytePos; - // Move past any tail bytes - while ( $bytePos > 0 && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0" ) { - --$bytePos; - } - } - } - - return $bytePos; + return Fallback::mb_substr_split_unicode( $str, $splitPos ); } } if ( !function_exists( 'mb_strlen' ) ) { - /** - * Fallback implementation of mb_strlen, hardcoded to UTF-8. - * @param string $str - * @param string $enc optional encoding; ignored - * @return int - */ + /** @codeCoverageIgnore */ function mb_strlen( $str, $enc = '' ) { - $counts = count_chars( $str ); - $total = 0; - - // Count ASCII bytes - for( $i = 0; $i < 0x80; $i++ ) { - $total += $counts[$i]; - } - - // Count multibyte sequence heads - for( $i = 0xc0; $i < 0xff; $i++ ) { - $total += $counts[$i]; - } - return $total; + return Fallback::mb_strlen( $str, $enc ); } } - if( !function_exists( 'mb_strpos' ) ) { - /** - * Fallback implementation of mb_strpos, hardcoded to UTF-8. - * @param $haystack String - * @param $needle String - * @param $offset String: optional start position - * @param $encoding String: optional encoding; ignored - * @return int - */ + /** @codeCoverageIgnore */ function mb_strpos( $haystack, $needle, $offset = 0, $encoding = '' ) { - $needle = preg_quote( $needle, '/' ); - - $ar = array(); - preg_match( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset ); - - if( isset( $ar[0][1] ) ) { - return $ar[0][1]; - } else { - return false; - } + return Fallback::mb_strpos( $haystack, $needle, $offset, $encoding ); } + } if( !function_exists( 'mb_strrpos' ) ) { - /** - * Fallback implementation of mb_strrpos, hardcoded to UTF-8. - * @param $haystack String - * @param $needle String - * @param $offset String: optional start position - * @param $encoding String: optional encoding; ignored - * @return int - */ + /** @codeCoverageIgnore */ function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = '' ) { - $needle = preg_quote( $needle, '/' ); - - $ar = array(); - preg_match_all( '/' . $needle . '/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset ); - - if( isset( $ar[0] ) && count( $ar[0] ) > 0 && - isset( $ar[0][count( $ar[0] ) - 1][1] ) ) { - return $ar[0][count( $ar[0] ) - 1][1]; - } else { - return false; - } + return Fallback::mb_strrpos( $haystack, $needle, $offset, $encoding ); } } + // Support for Wietse Venema's taint feature if ( !function_exists( 'istainted' ) ) { + /** @codeCoverageIgnore */ function istainted( $var ) { return 0; } + /** @codeCoverageIgnore */ function taint( $var, $level = 0 ) {} + /** @codeCoverageIgnore */ function untaint( $var, $level = 0 ) {} define( 'TC_HTML', 1 ); define( 'TC_SHELL', 1 ); @@ -197,25 +77,20 @@ if ( !function_exists( 'istainted' ) ) { define( 'TC_PCRE', 1 ); define( 'TC_SELF', 1 ); } - -// array_fill_keys() was only added in 5.2, but people use it anyway -// add a back-compat layer for 5.1. See bug 27781 -if( !function_exists( 'array_fill_keys' ) ) { - function array_fill_keys( $keys, $value ) { - return array_combine( $keys, array_fill( 0, count( $keys ), $value ) ); - } -} - - /// @endcond - /** * Like array_diff( $a, $b ) except that it works with two-dimensional arrays. */ function wfArrayDiff2( $a, $b ) { return array_udiff( $a, $b, 'wfArrayDiff2_cmp' ); } + +/** + * @param $a + * @param $b + * @return int + */ function wfArrayDiff2_cmp( $a, $b ) { if ( !is_array( $a ) ) { return strcmp( $a, $b ); @@ -235,12 +110,148 @@ function wfArrayDiff2_cmp( $a, $b ) { } /** - * Seed Mersenne Twister - * No-op for compatibility; only necessary in PHP < 4.2.0 - * @deprecated. Remove in 1.18 + * Array lookup + * Returns an array where the values in the first array are replaced by the + * values in the second array with the corresponding keys + * + * @param $a Array + * @param $b Array + * @return array + */ +function wfArrayLookup( $a, $b ) { + return array_flip( array_intersect( array_flip( $a ), array_keys( $b ) ) ); +} + +/** + * Appends to second array if $value differs from that in $default + * + * @param $key String|Int + * @param $value Mixed + * @param $default Mixed + * @param $changed Array to alter + */ +function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { + if ( is_null( $changed ) ) { + throw new MWException( 'GlobalFunctions::wfAppendToArrayIfNotDefault got null' ); + } + if ( $default[$key] !== $value ) { + $changed[$key] = $value; + } +} + +/** + * Backwards array plus for people who haven't bothered to read the PHP manual + * XXX: will not darn your socks for you. + * + * @param $array1 Array + * @param [$array2, [...]] Arrays + * @return Array + */ +function wfArrayMerge( $array1/* ... */ ) { + $args = func_get_args(); + $args = array_reverse( $args, true ); + $out = array(); + foreach ( $args as $arg ) { + $out += $arg; + } + return $out; +} + +/** + * Merge arrays in the style of getUserPermissionsErrors, with duplicate removal + * e.g. + * wfMergeErrorArrays( + * array( array( 'x' ) ), + * array( array( 'x', '2' ) ), + * array( array( 'x' ) ), + * array( array( 'y') ) + * ); + * returns: + * array( + * array( 'x', '2' ), + * array( 'x' ), + * array( 'y' ) + * ) + * @param varargs + * @return Array + */ +function wfMergeErrorArrays( /*...*/ ) { + $args = func_get_args(); + $out = array(); + foreach ( $args as $errors ) { + foreach ( $errors as $params ) { + # @todo FIXME: Sometimes get nested arrays for $params, + # which leads to E_NOTICEs + $spec = implode( "\t", $params ); + $out[$spec] = $params; + } + } + return array_values( $out ); +} + +/** + * Insert array into another array after the specified *KEY* + * + * @param $array Array: The array. + * @param $insert Array: The array to insert. + * @param $after Mixed: The key to insert after + * @return Array + */ +function wfArrayInsertAfter( $array, $insert, $after ) { + // Find the offset of the element to insert after. + $keys = array_keys( $array ); + $offsetByKey = array_flip( $keys ); + + $offset = $offsetByKey[$after]; + + // Insert at the specified offset + $before = array_slice( $array, 0, $offset + 1, true ); + $after = array_slice( $array, $offset + 1, count( $array ) - $offset, true ); + + $output = $before + $insert + $after; + + return $output; +} + +/** + * Recursively converts the parameter (an object) to an array with the same data + * + * @param $objOrArray Object|Array + * @param $recursive Bool + * @return Array + */ +function wfObjectToArray( $objOrArray, $recursive = true ) { + $array = array(); + if( is_object( $objOrArray ) ) { + $objOrArray = get_object_vars( $objOrArray ); + } + foreach ( $objOrArray as $key => $value ) { + if ( $recursive && ( is_object( $value ) || is_array( $value ) ) ) { + $value = wfObjectToArray( $value ); + } + + $array[$key] = $value; + } + + return $array; +} + +/** + * Wrapper around array_map() which also taints variables + * + * @param $function Callback + * @param $input Array + * @return Array */ -function wfSeedRandom() { - wfDeprecated(__FUNCTION__); +function wfArrayMap( $function, $input ) { + $ret = array_map( $function, $input ); + foreach ( $ret as $key => $value ) { + $taint = istainted( $input[$key] ); + if ( $taint ) { + taint( $ret[$key], $taint ); + } + } + return $ret; } /** @@ -283,6 +294,11 @@ function wfRandom() { */ function wfUrlencode( $s ) { static $needle; + if ( is_null( $s ) ) { + $needle = null; + return; + } + if ( is_null( $needle ) ) { $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F' ); if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) ) { @@ -300,6 +316,365 @@ function wfUrlencode( $s ) { return $s; } +/** + * This function takes two arrays as input, and returns a CGI-style string, e.g. + * "days=7&limit=100". Options in the first array override options in the second. + * Options set to "" will not be output. + * + * @param $array1 Array ( String|Array ) + * @param $array2 Array ( String|Array ) + * @param $prefix String + * @return String + */ +function wfArrayToCGI( $array1, $array2 = null, $prefix = '' ) { + if ( !is_null( $array2 ) ) { + $array1 = $array1 + $array2; + } + + $cgi = ''; + foreach ( $array1 as $key => $value ) { + if ( $value !== '' ) { + if ( $cgi != '' ) { + $cgi .= '&'; + } + if ( $prefix !== '' ) { + $key = $prefix . "[$key]"; + } + if ( is_array( $value ) ) { + $firstTime = true; + foreach ( $value as $k => $v ) { + $cgi .= $firstTime ? '' : '&'; + if ( is_array( $v ) ) { + $cgi .= wfArrayToCGI( $v, null, $key . "[$k]" ); + } else { + $cgi .= urlencode( $key . "[$k]" ) . '=' . urlencode( $v ); + } + $firstTime = false; + } + } else { + if ( is_object( $value ) ) { + $value = $value->__toString(); + } + $cgi .= urlencode( $key ) . '=' . urlencode( $value ); + } + } + } + return $cgi; +} + +/** + * This is the logical opposite of wfArrayToCGI(): it accepts a query string as + * its argument and returns the same string in array form. This allows compa- + * tibility with legacy functions that accept raw query strings instead of nice + * arrays. Of course, keys and values are urldecode()d. Don't try passing in- + * valid query strings, or it will explode. + * + * @param $query String: query string + * @return array Array version of input + */ +function wfCgiToArray( $query ) { + if ( isset( $query[0] ) && $query[0] == '?' ) { + $query = substr( $query, 1 ); + } + $bits = explode( '&', $query ); + $ret = array(); + foreach ( $bits as $bit ) { + if ( $bit === '' ) { + continue; + } + list( $key, $value ) = explode( '=', $bit ); + $key = urldecode( $key ); + $value = urldecode( $value ); + if ( strpos( $key, '[' ) !== false ) { + $keys = array_reverse( explode( '[', $key ) ); + $key = array_pop( $keys ); + $temp = $value; + foreach ( $keys as $k ) { + $k = substr( $k, 0, -1 ); + $temp = array( $k => $temp ); + } + if ( isset( $ret[$key] ) ) { + $ret[$key] = array_merge( $ret[$key], $temp ); + } else { + $ret[$key] = $temp; + } + } else { + $ret[$key] = $value; + } + } + return $ret; +} + +/** + * Append a query string to an existing URL, which may or may not already + * have query string parameters already. If so, they will be combined. + * + * @param $url String + * @param $query Mixed: string or associative array + * @return string + */ +function wfAppendQuery( $url, $query ) { + if ( is_array( $query ) ) { + $query = wfArrayToCGI( $query ); + } + if( $query != '' ) { + if( false === strpos( $url, '?' ) ) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= $query; + } + return $url; +} + +/** + * Expand a potentially local URL to a fully-qualified URL. Assumes $wgServer + * is correct. + * + * The meaning of the PROTO_* constants is as follows: + * PROTO_HTTP: Output a URL starting with http:// + * PROTO_HTTPS: Output a URL starting with https:// + * PROTO_RELATIVE: Output a URL starting with // (protocol-relative URL) + * PROTO_CURRENT: Output a URL starting with either http:// or https:// , depending on which protocol was used for the current incoming request + * PROTO_CANONICAL: For URLs without a domain, like /w/index.php , use $wgCanonicalServer. For protocol-relative URLs, use the protocol of $wgCanonicalServer + * PROTO_INTERNAL: Like PROTO_CANONICAL, but uses $wgInternalServer instead of $wgCanonicalServer + * + * @todo this won't work with current-path-relative URLs + * like "subdir/foo.html", etc. + * + * @param $url String: either fully-qualified or a local path + query + * @param $defaultProto Mixed: one of the PROTO_* constants. Determines the protocol to use if $url or $wgServer is protocol-relative + * @return string Fully-qualified URL + */ +function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) { + global $wgServer, $wgCanonicalServer, $wgInternalServer; + $serverUrl = $wgServer; + if ( $defaultProto === PROTO_CANONICAL ) { + $serverUrl = $wgCanonicalServer; + } + // Make $wgInternalServer fall back to $wgServer if not set + if ( $defaultProto === PROTO_INTERNAL && $wgInternalServer !== false ) { + $serverUrl = $wgInternalServer; + } + if ( $defaultProto === PROTO_CURRENT ) { + $defaultProto = WebRequest::detectProtocol() . '://'; + } + + // Analyze $serverUrl to obtain its protocol + $bits = wfParseUrl( $serverUrl ); + $serverHasProto = $bits && $bits['scheme'] != ''; + + if ( $defaultProto === PROTO_CANONICAL || $defaultProto === PROTO_INTERNAL ) { + if ( $serverHasProto ) { + $defaultProto = $bits['scheme'] . '://'; + } else { + // $wgCanonicalServer or $wgInternalServer doesn't have a protocol. This really isn't supposed to happen + // Fall back to HTTP in this ridiculous case + $defaultProto = PROTO_HTTP; + } + } + + $defaultProtoWithoutSlashes = substr( $defaultProto, 0, -2 ); + + if( substr( $url, 0, 2 ) == '//' ) { + return $defaultProtoWithoutSlashes . $url; + } elseif( substr( $url, 0, 1 ) == '/' ) { + // If $serverUrl is protocol-relative, prepend $defaultProtoWithoutSlashes, otherwise leave it alone + return ( $serverHasProto ? '' : $defaultProtoWithoutSlashes ) . $serverUrl . $url; + } else { + return $url; + } +} + +/** + * Returns a regular expression of url protocols + * + * @param $includeProtocolRelative bool If false, remove '//' from the returned protocol list. + * DO NOT USE this directy, use wfUrlProtocolsWithoutProtRel() instead + * @return String + */ +function wfUrlProtocols( $includeProtocolRelative = true ) { + global $wgUrlProtocols; + + // Cache return values separately based on $includeProtocolRelative + static $withProtRel = null, $withoutProtRel = null; + $cachedValue = $includeProtocolRelative ? $withProtRel : $withoutProtRel; + if ( !is_null( $cachedValue ) ) { + return $cachedValue; + } + + // Support old-style $wgUrlProtocols strings, for backwards compatibility + // with LocalSettings files from 1.5 + if ( is_array( $wgUrlProtocols ) ) { + $protocols = array(); + foreach ( $wgUrlProtocols as $protocol ) { + // Filter out '//' if !$includeProtocolRelative + if ( $includeProtocolRelative || $protocol !== '//' ) { + $protocols[] = preg_quote( $protocol, '/' ); + } + } + + $retval = implode( '|', $protocols ); + } else { + // Ignore $includeProtocolRelative in this case + // This case exists for pre-1.6 compatibility, and we can safely assume + // that '//' won't appear in a pre-1.6 config because protocol-relative + // URLs weren't supported until 1.18 + $retval = $wgUrlProtocols; + } + + // Cache return value + if ( $includeProtocolRelative ) { + $withProtRel = $retval; + } else { + $withoutProtRel = $retval; + } + return $retval; +} + +/** + * Like wfUrlProtocols(), but excludes '//' from the protocol list. Use this if + * you need a regex that matches all URL protocols but does not match protocol- + * relative URLs + */ +function wfUrlProtocolsWithoutProtRel() { + return wfUrlProtocols( false ); +} + +/** + * parse_url() work-alike, but non-broken. Differences: + * + * 1) Does not raise warnings on bad URLs (just returns false) + * 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly + * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2)) + * + * @param $url String: a URL to parse + * @return Array: bits of the URL in an associative array, per PHP docs + */ +function wfParseUrl( $url ) { + global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php + + // Protocol-relative URLs are handled really badly by parse_url(). It's so bad that the easiest + // way to handle them is to just prepend 'http:' and strip the protocol out later + $wasRelative = substr( $url, 0, 2 ) == '//'; + if ( $wasRelative ) { + $url = "http:$url"; + } + wfSuppressWarnings(); + $bits = parse_url( $url ); + wfRestoreWarnings(); + if ( !$bits ) { + return false; + } + + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it + if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) { + $bits['delimiter'] = '://'; + } elseif ( in_array( $bits['scheme'] . ':', $wgUrlProtocols ) ) { + $bits['delimiter'] = ':'; + // parse_url detects for news: and mailto: the host part of an url as path + // We have to correct this wrong detection + if ( isset( $bits['path'] ) ) { + $bits['host'] = $bits['path']; + $bits['path'] = ''; + } + } else { + return false; + } + + /* Provide an empty host for eg. file:/// urls (see bug 28627) */ + if ( !isset( $bits['host'] ) ) { + $bits['host'] = ''; + + /* parse_url loses the third / for file:///c:/ urls (but not on variants) */ + if ( substr( $bits['path'], 0, 1 ) !== '/' ) { + $bits['path'] = '/' . $bits['path']; + } + } + + // If the URL was protocol-relative, fix scheme and delimiter + if ( $wasRelative ) { + $bits['scheme'] = ''; + $bits['delimiter'] = '//'; + } + return $bits; +} + +/** + * Make URL indexes, appropriate for the el_index field of externallinks. + * + * @param $url String + * @return array + */ +function wfMakeUrlIndexes( $url ) { + $bits = wfParseUrl( $url ); + + // Reverse the labels in the hostname, convert to lower case + // For emails reverse domainpart only + if ( $bits['scheme'] == 'mailto' ) { + $mailparts = explode( '@', $bits['host'], 2 ); + if ( count( $mailparts ) === 2 ) { + $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); + } else { + // No domain specified, don't mangle it + $domainpart = ''; + } + $reversedHost = $domainpart . '@' . $mailparts[0]; + } else { + $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); + } + // Add an extra dot to the end + // Why? Is it in wrong place in mailto links? + if ( substr( $reversedHost, -1, 1 ) !== '.' ) { + $reversedHost .= '.'; + } + // Reconstruct the pseudo-URL + $prot = $bits['scheme']; + $index = $prot . $bits['delimiter'] . $reversedHost; + // Leave out user and password. Add the port, path, query and fragment + if ( isset( $bits['port'] ) ) { + $index .= ':' . $bits['port']; + } + if ( isset( $bits['path'] ) ) { + $index .= $bits['path']; + } else { + $index .= '/'; + } + if ( isset( $bits['query'] ) ) { + $index .= '?' . $bits['query']; + } + if ( isset( $bits['fragment'] ) ) { + $index .= '#' . $bits['fragment']; + } + + if ( $prot == '' ) { + return array( "http:$index", "https:$index" ); + } else { + return array( $index ); + } +} + +/** + * Check whether a given URL has a domain that occurs in a given set of domains + * @param $url string URL + * @param $domains array Array of domains (strings) + * @return bool True if the host part of $url ends in one of the strings in $domains + */ +function wfMatchesDomainList( $url, $domains ) { + $bits = wfParseUrl( $url ); + if ( is_array( $bits ) && isset( $bits['host'] ) ) { + foreach ( (array)$domains as $domain ) { + // FIXME: This gives false positives. http://nds-nl.wikipedia.org will match nl.wikipedia.org + // We should use something that interprets dots instead + if ( substr( $bits['host'], -strlen( $domain ) ) === $domain ) { + return true; + } + } + } + return false; +} + /** * Sends a line to the debug log if enabled or, optionally, to a comment in output. * In normal operation this is a NOP. @@ -316,44 +691,61 @@ function wfUrlencode( $s ) { function wfDebug( $text, $logonly = false ) { global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage; global $wgDebugLogPrefix, $wgShowDebug; - static $recursion = 0; static $cache = array(); // Cache of unoutputted messages $text = wfDebugTimer() . $text; - # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet - if ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' && !$wgDebugRawPage ) { + if ( !$wgDebugRawPage && wfIsDebugRawPage() ) { return; } if ( ( $wgDebugComments || $wgShowDebug ) && !$logonly ) { $cache[] = $text; - if ( !isset( $wgOut ) ) { - return; + if ( isset( $wgOut ) && is_object( $wgOut ) ) { + // add the message and any cached messages to the output + array_map( array( $wgOut, 'debug' ), $cache ); + $cache = array(); } - if ( !StubObject::isRealObject( $wgOut ) ) { - if ( $recursion ) { - return; - } - $recursion++; - $wgOut->_unstub(); - $recursion--; + } + if ( wfRunHooks( 'Debug', array( $text, null /* no log group */ ) ) ) { + if ( $wgDebugLogFile != '' && !$wgProfileOnly ) { + # Strip unprintables; they can switch terminal modes when binary data + # gets dumped, which is pretty annoying. + $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text ); + $text = $wgDebugLogPrefix . $text; + wfErrorLog( $text, $wgDebugLogFile ); } + } +} - // add the message and possible cached ones to the output - array_map( array( $wgOut, 'debug' ), $cache ); - $cache = array(); +/** + * Returns true if debug logging should be suppressed if $wgDebugRawPage = false + */ +function wfIsDebugRawPage() { + static $cache; + if ( $cache !== null ) { + return $cache; } - if ( $wgDebugLogFile != '' && !$wgProfileOnly ) { - # Strip unprintables; they can switch terminal modes when binary data - # gets dumped, which is pretty annoying. - $text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text ); - $text = $wgDebugLogPrefix . $text; - wfErrorLog( $text, $wgDebugLogFile ); + # Check for raw action using $_GET not $wgRequest, since the latter might not be initialised yet + if ( ( isset( $_GET['action'] ) && $_GET['action'] == 'raw' ) + || ( + isset( $_SERVER['SCRIPT_NAME'] ) + && substr( $_SERVER['SCRIPT_NAME'], -8 ) == 'load.php' + ) ) + { + $cache = true; + } else { + $cache = false; } + return $cache; } +/** + * Get microsecond timestamps for debug logs + * + * @return string + */ function wfDebugTimer() { global $wgDebugTimestamps; if ( !$wgDebugTimestamps ) { @@ -373,6 +765,7 @@ function wfDebugTimer() { /** * Send a line giving PHP memory usage. + * * @param $exact Bool: print exact values instead of kilobytes (default: false) */ function wfDebugMem( $exact = false ) { @@ -405,7 +798,9 @@ function wfDebugLog( $logGroup, $text, $public = true ) { } else { $host = ''; } - wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] ); + if ( wfRunHooks( 'Debug', array( $text, $logGroup ) ) ) { + wfErrorLog( "$time $host $wiki: $text", $wgDebugLogGroups[$logGroup] ); + } } elseif ( $public === true ) { wfDebug( $text, true ); } @@ -413,6 +808,7 @@ function wfDebugLog( $logGroup, $text, $public = true ) { /** * Log for database errors + * * @param $text String: database error message. */ function wfLogDBError( $text ) { @@ -429,6 +825,9 @@ function wfLogDBError( $text ) { * * Can also log to TCP or UDP with the syntax udp://host:port/prefix. This will * send lines to the specified port, prefixed by the specified prefix and a space. + * + * @param $text String + * @param $file String filename */ function wfErrorLog( $text, $file ) { if ( substr( $file, 0, 4 ) == 'udp:' ) { @@ -450,12 +849,21 @@ function wfErrorLog( $text, $file ) { } else { throw new MWException( __METHOD__ . ': Invalid UDP specification' ); } + // Clean it up for the multiplexer if ( strval( $prefix ) !== '' ) { $text = preg_replace( '/^/m', $prefix . ' ', $text ); + + // Limit to 64KB + if ( strlen( $text ) > 65534 ) { + $text = substr( $text, 0, 65534 ); + } + if ( substr( $text, -1 ) != "\n" ) { $text .= "\n"; } + } elseif ( strlen( $text ) > 65535 ) { + $text = substr( $text, 0, 65535 ); } $sock = socket_create( $domain, SOCK_DGRAM, SOL_UDP ); @@ -469,7 +877,7 @@ function wfErrorLog( $text, $file ) { $exists = file_exists( $file ); $size = $exists ? filesize( $file ) : false; if ( !$exists || ( $size !== false && $size + strlen( $text ) < 0x7fffffff ) ) { - error_log( $text, 3, $file ); + file_put_contents( $file, $text, FILE_APPEND ); } wfRestoreWarnings(); } @@ -480,49 +888,60 @@ function wfErrorLog( $text, $file ) { */ function wfLogProfilingData() { global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; - global $wgProfiler, $wgProfileLimit, $wgUser; + global $wgProfileLimit, $wgUser; + + $profiler = Profiler::instance(); + # Profiling must actually be enabled... - if( is_null( $wgProfiler ) ) { + if ( $profiler->isStub() ) { return; } - # Get total page request time + + // Get total page request time and only show pages that longer than + // $wgProfileLimit time (default is 0) $now = wfTime(); $elapsed = $now - $wgRequestTime; - # Only show pages that longer than $wgProfileLimit time (default is 0) - if( $elapsed <= $wgProfileLimit ) { + if ( $elapsed <= $wgProfileLimit ) { return; } - $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); + + $profiler->logData(); + + // Check whether this should be logged in the debug file. + if ( $wgDebugLogFile == '' || ( !$wgDebugRawPage && wfIsDebugRawPage() ) ) { + return; + } + $forward = ''; - if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + if ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $forward = ' forwarded for ' . $_SERVER['HTTP_X_FORWARDED_FOR']; } - if( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { + if ( !empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { $forward .= ' client IP ' . $_SERVER['HTTP_CLIENT_IP']; } - if( !empty( $_SERVER['HTTP_FROM'] ) ) { + if ( !empty( $_SERVER['HTTP_FROM'] ) ) { $forward .= ' from ' . $_SERVER['HTTP_FROM']; } - if( $forward ) { + if ( $forward ) { $forward = "\t(proxied via {$_SERVER['REMOTE_ADDR']}{$forward})"; } - // Don't unstub $wgUser at this late stage just for statistics purposes - // FIXME: We can detect some anons even if it is not loaded. See User::getId() - if( $wgUser->mDataLoaded && $wgUser->isAnon() ) { + // Don't load $wgUser at this late stage just for statistics purposes + // @todo FIXME: We can detect some anons even if it is not loaded. See User::getId() + if ( $wgUser->isItemLoaded( 'id' ) && $wgUser->isAnon() ) { $forward .= ' anon'; } $log = sprintf( "%s\t%04.3f\t%s\n", gmdate( 'YmdHis' ), $elapsed, urldecode( $wgRequest->getRequestURL() . $forward ) ); - if ( $wgDebugLogFile != '' && ( $wgRequest->getVal( 'action' ) != 'raw' || $wgDebugRawPage ) ) { - wfErrorLog( $log . $prof, $wgDebugLogFile ); - } + + wfErrorLog( $log . $profiler->getOutput(), $wgDebugLogFile ); } /** * Check if the wiki read-only lock file is present. This can be used to lock * off editing functions, but doesn't guarantee that the database will not be * modified. + * * @return bool */ function wfReadOnly() { @@ -551,6 +970,7 @@ function wfReadOnlyReason() { /** * Return a Language object from $langcode + * * @param $langcode Mixed: either: * - a Language object * - code of the language to get the message for, if it is @@ -596,15 +1016,15 @@ function wfGetLangObj( $langcode = false ) { } /** - * Use this instead of $wgContLang, when working with user interface. - * User interface is currently hard coded according to wiki content language - * in many ways, especially regarding to text direction. There is lots stuff - * to fix, hence this function to keep the old behaviour unless the global - * $wgBetterDirectionality is enabled (or removed when everything works). + * Old function when $wgBetterDirectionality existed + * Removed in core, kept in extensions for backwards compat. + * + * @deprecated since 1.18 + * @return Language */ function wfUILang() { - global $wgBetterDirectionality; - return wfGetLangObj( !$wgBetterDirectionality ); + global $wgLang; + return $wgLang; } /** @@ -613,7 +1033,7 @@ function wfUILang() { * The intention is that this function replaces all old wfMsg* functions. * @param $key \string Message key. * Varargs: normal message parameters. - * @return \type{Message} + * @return Message * @since 1.17 */ function wfMessage( $key /*...*/) { @@ -625,6 +1045,19 @@ function wfMessage( $key /*...*/) { return new Message( $key, $params ); } +/** + * This function accepts multiple message keys and returns a message instance + * for the first message which is non-empty. If all messages are empty then an + * instance of the first message key is returned. + * @param varargs: message keys + * @return Message + * @since 1.18 + */ +function wfMessageFallback( /*...*/ ) { + $args = func_get_args(); + return MWFunction::callArray( 'Message::newFallbackSequence', $args ); +} + /** * Get a message from anywhere, for the current user language. * @@ -634,18 +1067,25 @@ function wfMessage( $key /*...*/) { * @param $key String: lookup key for the message, usually * defined in languages/Language.php * - * This function also takes extra optional parameters (not - * shown in the function definition), which can be used to - * insert variable text into the predefined message. + * Parameters to the message, which can be used to insert variable text into + * it, can be passed to this function in the following formats: + * - One per argument, starting at the second parameter + * - As an array in the second parameter + * These are not shown in the function definition. + * + * @return String */ function wfMsg( $key ) { $args = func_get_args(); array_shift( $args ); - return wfMsgReal( $key, $args, true ); + return wfMsgReal( $key, $args ); } /** * Same as above except doesn't transform the message + * + * @param $key String + * @return String */ function wfMsgNoTrans( $key ) { $args = func_get_args(); @@ -673,7 +1113,8 @@ function wfMsgNoTrans( $key ) { * order to, e.g., fix a link in every possible language. * * @param $key String: lookup key for the message, usually - * defined in languages/Language.php + * defined in languages/Language.php + * @return String */ function wfMsgForContent( $key ) { global $wgForceUIMsgAsContentMsg; @@ -690,6 +1131,9 @@ function wfMsgForContent( $key ) { /** * Same as above except doesn't transform the message + * + * @param $key String + * @return String */ function wfMsgForContentNoTrans( $key ) { global $wgForceUIMsgAsContentMsg; @@ -697,41 +1141,16 @@ function wfMsgForContentNoTrans( $key ) { array_shift( $args ); $forcontent = true; if( is_array( $wgForceUIMsgAsContentMsg ) && - in_array( $key, $wgForceUIMsgAsContentMsg ) ) - { - $forcontent = false; - } - return wfMsgReal( $key, $args, true, $forcontent, false ); -} - -/** - * Get a message from the language file, for the UI elements - */ -function wfMsgNoDB( $key ) { - $args = func_get_args(); - array_shift( $args ); - return wfMsgReal( $key, $args, false ); -} - -/** - * Get a message from the language file, for the content - */ -function wfMsgNoDBForContent( $key ) { - global $wgForceUIMsgAsContentMsg; - $args = func_get_args(); - array_shift( $args ); - $forcontent = true; - if( is_array( $wgForceUIMsgAsContentMsg ) && - in_array( $key, $wgForceUIMsgAsContentMsg ) ) + in_array( $key, $wgForceUIMsgAsContentMsg ) ) { $forcontent = false; } - return wfMsgReal( $key, $args, false, $forcontent ); + return wfMsgReal( $key, $args, true, $forcontent, false ); } - /** * Really get a message + * * @param $key String: key to get. * @param $args * @param $useDB Boolean @@ -747,21 +1166,9 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform return $message; } -/** - * This function provides the message source for messages to be edited which are *not* stored in the database. - * @param $key String: - */ -function wfMsgWeirdKey( $key ) { - $source = wfMsgGetKey( $key, false, true, false ); - if ( wfEmptyMsg( $key, $source ) ) { - return ''; - } else { - return $source; - } -} - /** * Fetch a message string value, but don't replace any keys yet. + * * @param $key String * @param $useDB Bool * @param $langCode String: Code of the language to get the message for, or @@ -769,20 +1176,15 @@ function wfMsgWeirdKey( $key ) { * @param $transform Boolean: whether to parse magic words, etc. * @return string */ -function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) { - global $wgMessageCache; - +function wfMsgGetKey( $key, $useDB = true, $langCode = false, $transform = true ) { wfRunHooks( 'NormalizeMessageKey', array( &$key, &$useDB, &$langCode, &$transform ) ); - if ( !is_object( $wgMessageCache ) ) { - throw new MWException( 'Trying to get message before message cache is initialised' ); - } - - $message = $wgMessageCache->get( $key, $useDB, $langCode ); + $cache = MessageCache::singleton(); + $message = $cache->get( $key, $useDB, $langCode ); if( $message === false ) { $message = '<' . htmlspecialchars( $key ) . '>'; } elseif ( $transform ) { - $message = $wgMessageCache->transform( $message ); + $message = $cache->transform( $message ); } return $message; } @@ -829,7 +1231,7 @@ function wfMsgReplaceArgs( $message, $args ) { function wfMsgHtml( $key ) { $args = func_get_args(); array_shift( $args ); - return wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $key, true ) ), $args ); + return wfMsgReplaceArgs( htmlspecialchars( wfMsgGetKey( $key ) ), $args ); } /** @@ -844,10 +1246,11 @@ function wfMsgHtml( $key ) { * @return string */ function wfMsgWikiHtml( $key ) { - global $wgOut; $args = func_get_args(); array_shift( $args ); - return wfMsgReplaceArgs( $wgOut->parse( wfMsgGetKey( $key, true ), /* can't be set to false */ true ), $args ); + return wfMsgReplaceArgs( + MessageCache::singleton()->parse( wfMsgGetKey( $key ), null, /* can't be set to false */ true )->getText(), + $args ); } /** @@ -864,13 +1267,12 @@ function wfMsgWikiHtml( $key ) { * content: fetch message for content language instead of interface * Also can accept a single associative argument, of the form 'language' => 'xx': * language: Language object or language code to fetch message for - * (overriden by content), its behaviour with parse, parseinline - * and parsemag is undefined. + * (overriden by content). * Behavior for conflicting options (e.g., parse+parseinline) is undefined. + * + * @return String */ function wfMsgExt( $key, $options ) { - global $wgOut; - $args = func_get_args(); array_shift( $args ); array_shift( $args ); @@ -891,12 +1293,15 @@ function wfMsgExt( $key, $options ) { if( in_array( 'content', $options, true ) ) { $forContent = true; $langCode = true; + $langCodeObj = null; } elseif( array_key_exists( 'language', $options ) ) { $forContent = false; $langCode = wfGetLangObj( $options['language'] ); + $langCodeObj = $langCode; } else { $forContent = false; $langCode = false; + $langCodeObj = null; } $string = wfMsgGetKey( $key, /*DB*/true, $langCode, /*Transform*/false ); @@ -905,21 +1310,18 @@ function wfMsgExt( $key, $options ) { $string = wfMsgReplaceArgs( $string, $args ); } + $messageCache = MessageCache::singleton(); if( in_array( 'parse', $options, true ) ) { - $string = $wgOut->parse( $string, true, !$forContent ); + $string = $messageCache->parse( $string, null, true, !$forContent, $langCodeObj )->getText(); } elseif ( in_array( 'parseinline', $options, true ) ) { - $string = $wgOut->parse( $string, true, !$forContent ); + $string = $messageCache->parse( $string, null, true, !$forContent, $langCodeObj )->getText(); $m = array(); if( preg_match( '/^

    (.*)\n?<\/p>\n?$/sU', $string, $m ) ) { $string = $m[1]; } } elseif ( in_array( 'parsemag', $options, true ) ) { - global $wgMessageCache; - if ( isset( $wgMessageCache ) ) { - $string = $wgMessageCache->transform( $string, - !$forContent, - is_object( $langCode ) ? $langCode : null ); - } + $string = $messageCache->transform( $string, + !$forContent, $langCodeObj ); } if ( in_array( 'escape', $options, true ) ) { @@ -935,58 +1337,16 @@ function wfMsgExt( $key, $options ) { return $string; } - /** - * Just like exit() but makes a note of it. - * Commits open transactions except if the error parameter is set + * Since wfMsg() and co suck, they don't return false if the message key they + * looked up didn't exist but a XHTML string, this function checks for the + * nonexistance of messages by checking the MessageCache::get() result directly. * - * @deprecated Please return control to the caller or throw an exception. Will - * be removed in 1.19. - */ -function wfAbruptExit( $error = false ) { - static $called = false; - if ( $called ) { - exit( -1 ); - } - $called = true; - - wfDeprecated( __FUNCTION__ ); - $bt = wfDebugBacktrace(); - if( $bt ) { - for( $i = 0; $i < count( $bt ); $i++ ) { - $file = isset( $bt[$i]['file'] ) ? $bt[$i]['file'] : 'unknown'; - $line = isset( $bt[$i]['line'] ) ? $bt[$i]['line'] : 'unknown'; - wfDebug( "WARNING: Abrupt exit in $file at line $line\n"); - } - } else { - wfDebug( "WARNING: Abrupt exit\n" ); - } - - wfLogProfilingData(); - - if ( !$error ) { - wfGetLB()->closeAll(); - } - exit( -1 ); -} - -/** - * @deprecated Please return control the caller or throw an exception. Will - * be removed in 1.19. - */ -function wfErrorExit() { - wfDeprecated( __FUNCTION__ ); - wfAbruptExit( true ); -} - -/** - * Print a simple message and die, returning nonzero to the shell if any. - * Plain die() fails to return nonzero to the shell if you pass a string. - * @param $msg String + * @param $key String: the message key looked up + * @return Boolean True if the message *doesn't* exist. */ -function wfDie( $msg = '' ) { - echo $msg; - die( 1 ); +function wfEmptyMsg( $key ) { + return MessageCache::singleton()->get( $key, /*useDB*/true, /*content*/false ) === false; } /** @@ -1003,6 +1363,7 @@ function wfDebugDieBacktrace( $msg = '' ) { * Fetch server name for use in error reporting etc. * Use real server name if available, so we know which machine * in a server farm generated the current page. + * * @return string */ function wfHostname() { @@ -1010,7 +1371,7 @@ function wfHostname() { if ( is_null( $host ) ) { if ( function_exists( 'posix_uname' ) ) { // This function not present on Windows - $uname = @posix_uname(); + $uname = posix_uname(); } else { $uname = false; } @@ -1030,6 +1391,7 @@ function wfHostname() { /** * Returns a HTML comment with the elapsed time since request. * This method has no side effects. + * * @return string */ function wfReportTime() { @@ -1054,9 +1416,11 @@ function wfReportTime() { * debug_backtrace is disabled, otherwise the output from * debug_backtrace() (trimmed). * + * @param $limit int This parameter can be used to limit the number of stack frames returned + * * @return array of backtrace information */ -function wfDebugBacktrace() { +function wfDebugBacktrace( $limit = 0 ) { static $disabled = null; if( extension_loaded( 'Zend Optimizer' ) ) { @@ -1078,9 +1442,18 @@ function wfDebugBacktrace() { return array(); } - return array_slice( debug_backtrace(), 1 ); + if ( $limit && version_compare( PHP_VERSION, '5.4.0', '>=' ) ) { + return array_slice( debug_backtrace( DEBUG_BACKTRACE_PROVIDE_OBJECT, $limit ), 1 ); + } else { + return array_slice( debug_backtrace(), 1 ); + } } +/** + * Get a debug backtrace as a string + * + * @return string + */ function wfBacktrace() { global $wgCommandLineMode; @@ -1108,7 +1481,7 @@ function wfBacktrace() { $msg .= '

  • ' . $file . ' line ' . $line . ' calls '; } if( !empty( $call['class'] ) ) { - $msg .= $call['class'] . '::'; + $msg .= $call['class'] . $call['type']; } $msg .= $call['function'] . '()'; @@ -1127,12 +1500,61 @@ function wfBacktrace() { return $msg; } +/** + * Get the name of the function which called this function + * + * @param $level Int + * @return Bool|string + */ +function wfGetCaller( $level = 2 ) { + $backtrace = wfDebugBacktrace( $level ); + if ( isset( $backtrace[$level] ) ) { + return wfFormatStackFrame( $backtrace[$level] ); + } else { + $caller = 'unknown'; + } + return $caller; +} + +/** + * Return a string consisting of callers in the stack. Useful sometimes + * for profiling specific points. + * + * @param $limit The maximum depth of the stack frame to return, or false for + * the entire stack. + * @return String + */ +function wfGetAllCallers( $limit = 3 ) { + $trace = array_reverse( wfDebugBacktrace() ); + if ( !$limit || $limit > count( $trace ) - 1 ) { + $limit = count( $trace ) - 1; + } + $trace = array_slice( $trace, -$limit - 1, $limit ); + return implode( '/', array_map( 'wfFormatStackFrame', $trace ) ); +} + +/** + * Return a string representation of frame + * + * @param $frame Array + * @return Bool + */ +function wfFormatStackFrame( $frame ) { + return isset( $frame['class'] ) ? + $frame['class'] . '::' . $frame['function'] : + $frame['function']; +} + /* Some generic result counters, pulled out of SearchEngine */ /** * @todo document + * + * @param $offset Int + * @param $limit Int + * @return String */ function wfShowingResults( $offset, $limit ) { global $wgLang; @@ -1144,32 +1566,20 @@ function wfShowingResults( $offset, $limit ) { ); } -/** - * @todo document - */ -function wfShowingResultsNum( $offset, $limit, $num ) { - global $wgLang; - return wfMsgExt( - 'showingresultsnum', - array( 'parseinline' ), - $wgLang->formatNum( $limit ), - $wgLang->formatNum( $offset + 1 ), - $wgLang->formatNum( $num ) - ); -} - /** * Generate (prev x| next x) (20|50|100...) type links for paging + * * @param $offset String * @param $limit Integer * @param $link String * @param $query String: optional URL query parameter string * @param $atend Bool: optional param for specified if this is the last page + * @return String */ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { global $wgLang; $fmtLimit = $wgLang->formatNum( $limit ); - // FIXME: Why on earth this needs one message for the text and another one for tooltip?? + // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip? # Get prev/next link display text $prev = wfMsgExt( 'prevn', array( 'parsemag', 'escape' ), $fmtLimit ); $next = wfMsgExt( 'nextn', array( 'parsemag', 'escape' ), $fmtLimit ); @@ -1221,6 +1631,7 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { /** * Generate links for (20|50|100...) items-per-page links + * * @param $offset String * @param $limit Integer * @param $title Title @@ -1242,16 +1653,17 @@ function wfNumLink( $offset, $limit, $title, $query = '' ) { /** * @todo document - * @todo FIXME: we may want to blacklist some broken browsers + * @todo FIXME: We may want to blacklist some broken browsers * + * @param $force Bool * @return bool Whereas client accept gzip compression */ -function wfClientAcceptsGzip() { +function wfClientAcceptsGzip( $force = false ) { static $result = null; - if ( $result === null ) { + if ( $result === null || $force ) { $result = false; if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) { - # FIXME: we may want to blacklist some broken browsers + # @todo FIXME: We may want to blacklist some broken browsers $m = array(); if( preg_match( '/\bgzip(?:;(q)=([0-9]+(?:\.[0-9]+)))?\b/', @@ -1275,8 +1687,8 @@ function wfClientAcceptsGzip() { * Obtain the offset and limit values from the request string; * used in special pages * - * @param $deflimit Default limit if none supplied - * @param $optionname Name of a user preference to check against + * @param $deflimit Int default limit if none supplied + * @param $optionname String Name of a user preference to check against * @return array * */ @@ -1289,50 +1701,26 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) { * Escapes the given text so that it may be output using addWikiText() * without any linking, formatting, etc. making its way through. This * is achieved by substituting certain characters with HTML entities. - * As required by the callers, is not used. It currently does - * not filter out characters which have special meaning only at the - * start of a line, such as "*". + * As required by the callers, is not used. * * @param $text String: text to be escaped + * @return String */ function wfEscapeWikiText( $text ) { - $text = str_replace( - array( '[', '|', ']', '\'', 'ISBN ', - 'RFC ', '://', "\n=", '{{', '}}' ), - array( '[', '|', ']', ''', 'ISBN ', - 'RFC ', '://', "\n=", '{{', '}}' ), - htmlspecialchars( $text ) - ); - return $text; -} - -/** - * @todo document - */ -function wfQuotedPrintable( $string, $charset = '' ) { - # Probably incomplete; see RFC 2045 - if( empty( $charset ) ) { - global $wgInputEncoding; - $charset = $wgInputEncoding; - } - $charset = strtoupper( $charset ); - $charset = str_replace( 'ISO-8859', 'ISO8859', $charset ); // ? - - $illegal = '\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\xff='; - $replace = $illegal . '\t ?_'; - if( !preg_match( "/[$illegal]/", $string ) ) { - return $string; - } - $out = "=?$charset?Q?"; - $out .= preg_replace( "/([$replace])/e", 'sprintf("=%02X",ord("$1"))', $string ); - $out .= '?='; - return $out; + $text = strtr( "\n$text", array( + '"' => '"', '&' => '&', "'" => ''', '<' => '<', + '=' => '=', '>' => '>', '[' => '[', ']' => ']', + '{' => '{', '|' => '|', '}' => '}', + "\n#" => "\n#", "\n*" => "\n*", + "\n:" => "\n:", "\n;" => "\n;", + '://' => '://', 'ISBN ' => 'ISBN ', 'RFC ' => 'RFC ', + ) ); + return substr( $text, 1 ); } - /** - * @todo document - * @return float + * Get the current unix timetstamp with microseconds. Useful for profiling + * @return Float */ function wfTime() { return microtime( true ); @@ -1342,6 +1730,11 @@ function wfTime() { * Sets dest to source and returns the original value of dest * If source is NULL, it just returns the value, it doesn't set the variable * If force is true, it will set the value even if source is NULL + * + * @param $dest Mixed + * @param $source Mixed + * @param $force Bool + * @return Mixed */ function wfSetVar( &$dest, $source, $force = false ) { $temp = $dest; @@ -1353,6 +1746,10 @@ function wfSetVar( &$dest, $source, $force = false ) { /** * As for wfSetVar except setting a bit + * + * @param $dest Int + * @param $bit Int + * @param $state Bool */ function wfSetBit( &$dest, $bit, $state = true ) { $temp = (bool)( $dest & $bit ); @@ -1366,115 +1763,6 @@ function wfSetBit( &$dest, $bit, $state = true ) { return $temp; } -/** - * This function takes two arrays as input, and returns a CGI-style string, e.g. - * "days=7&limit=100". Options in the first array override options in the second. - * Options set to "" will not be output. - */ -function wfArrayToCGI( $array1, $array2 = null ) { - if ( !is_null( $array2 ) ) { - $array1 = $array1 + $array2; - } - - $cgi = ''; - foreach ( $array1 as $key => $value ) { - if ( $value !== '' ) { - if ( $cgi != '' ) { - $cgi .= '&'; - } - if ( is_array( $value ) ) { - $firstTime = true; - foreach ( $value as $v ) { - $cgi .= ( $firstTime ? '' : '&') . - urlencode( $key . '[]' ) . '=' . - urlencode( $v ); - $firstTime = false; - } - } else { - if ( is_object( $value ) ) { - $value = $value->__toString(); - } - $cgi .= urlencode( $key ) . '=' . - urlencode( $value ); - } - } - } - return $cgi; -} - -/** - * This is the logical opposite of wfArrayToCGI(): it accepts a query string as - * its argument and returns the same string in array form. This allows compa- - * tibility with legacy functions that accept raw query strings instead of nice - * arrays. Of course, keys and values are urldecode()d. Don't try passing in- - * valid query strings, or it will explode. - * - * @param $query String: query string - * @return array Array version of input - */ -function wfCgiToArray( $query ) { - if( isset( $query[0] ) && $query[0] == '?' ) { - $query = substr( $query, 1 ); - } - $bits = explode( '&', $query ); - $ret = array(); - foreach( $bits as $bit ) { - if( $bit === '' ) { - continue; - } - list( $key, $value ) = explode( '=', $bit ); - $key = urldecode( $key ); - $value = urldecode( $value ); - $ret[$key] = $value; - } - return $ret; -} - -/** - * Append a query string to an existing URL, which may or may not already - * have query string parameters already. If so, they will be combined. - * - * @param $url String - * @param $query Mixed: string or associative array - * @return string - */ -function wfAppendQuery( $url, $query ) { - if ( is_array( $query ) ) { - $query = wfArrayToCGI( $query ); - } - if( $query != '' ) { - if( false === strpos( $url, '?' ) ) { - $url .= '?'; - } else { - $url .= '&'; - } - $url .= $query; - } - return $url; -} - -/** - * Expand a potentially local URL to a fully-qualified URL. Assumes $wgServer - * and $wgProto are correct. - * - * @todo this won't work with current-path-relative URLs - * like "subdir/foo.html", etc. - * - * @param $url String: either fully-qualified or a local path + query - * @return string Fully-qualified URL - */ -function wfExpandUrl( $url ) { - if( substr( $url, 0, 2 ) == '//' ) { - global $wgProto; - return $wgProto . ':' . $url; - } elseif( substr( $url, 0, 1 ) == '/' ) { - global $wgServer; - return $wgServer . $url; - } else { - return $url; - } -} - /** * Windows-compatible version of escapeshellarg() * Windows doesn't recognise single-quotes in the shell, but the escapeshellarg() @@ -1482,6 +1770,9 @@ function wfExpandUrl( $url ) { * * Also fixes the locale problems on Linux in PHP 5.2.6+ (bug backported to * earlier distro releases of PHP) + * + * @param varargs + * @return String */ function wfEscapeShellArg( ) { wfInitShellLocale(); @@ -1497,8 +1788,12 @@ function wfEscapeShellArg( ) { } if ( wfIsWindows() ) { - // Escaping for an MSVC-style command line parser - // Ref: http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html + // Escaping for an MSVC-style command line parser and CMD.EXE + // Refs: + // * http://web.archive.org/web/20020708081031/http://mailman.lyra.org/pipermail/scite-interest/2002-March/000436.html + // * http://technet.microsoft.com/en-us/library/cc723564.aspx + // * Bug #13518 + // * CR r63214 // Double the backslashes before any double quotes. Escape the double quotes. $tokens = preg_split( '/(\\\\*")/', $arg, -1, PREG_SPLIT_DELIM_CAPTURE ); $arg = ''; @@ -1535,6 +1830,12 @@ function wfEscapeShellArg( ) { /** * wfMerge attempts to merge differences between three texts. * Returns true for a clean merge and false for failure or a conflict. + * + * @param $old String + * @param $mine String + * @param $yours String + * @param $result String + * @return Bool */ function wfMerge( $old, $mine, $yours, &$result ) { global $wgDiff3; @@ -1604,6 +1905,7 @@ function wfMerge( $old, $mine, $yours, &$result ) { /** * Returns unified plain-text diff of two texts. * Useful for machine processing of diffs. + * * @param $before String: the text before the changes. * @param $after String: the text after the changes. * @param $params String: command-line options for the diff command. @@ -1681,7 +1983,7 @@ function wfDiff( $before, $after, $params = '-u' ) { function wfVarDump( $var ) { global $wgOut; $s = str_replace( "\n", "
    \n", var_export( $var, true ) . "\n" ); - if ( headers_sent() || !@is_object( $wgOut ) ) { + if ( headers_sent() || !isset( $wgOut ) || !is_object( $wgOut ) ) { print $s; } else { $wgOut->addHTML( $s ); @@ -1690,6 +1992,10 @@ function wfVarDump( $var ) { /** * Provide a simple HTTP error. + * + * @param $code Int|String + * @param $label String + * @param $desc String */ function wfHttpError( $code, $label, $desc ) { global $wgOut; @@ -1783,6 +2089,10 @@ function wfClearOutputBuffers() { /** * Converts an Accept-* header into an array mapping string values to quality * factors + * + * @param $accept String + * @param $def String default + * @return Array */ function wfAcceptToPrefs( $accept, $def = '*/*' ) { # No arg means accept anything (per HTTP spec) @@ -1795,13 +2105,13 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) { $parts = explode( ',', $accept ); foreach( $parts as $part ) { - # FIXME: doesn't deal with params like 'text/html; level=1' - @list( $value, $qpart ) = explode( ';', trim( $part ) ); + # @todo FIXME: Doesn't deal with params like 'text/html; level=1' + $values = explode( ';', trim( $part ) ); $match = array(); - if( !isset( $qpart ) ) { - $prefs[$value] = 1.0; - } elseif( preg_match( '/q\s*=\s*(\d*\.\d+)/', $qpart, $match ) ) { - $prefs[$value] = floatval( $match[1] ); + if ( count( $values ) == 1 ) { + $prefs[$values[0]] = 1.0; + } elseif ( preg_match( '/q\s*=\s*(\d*\.\d+)/', $values[1], $match ) ) { + $prefs[$values[0]] = floatval( $match[1] ); } } @@ -1845,7 +2155,7 @@ function mimeTypeMatch( $type, $avail ) { * @param $sprefs Array: server's offered types * @return string * - * @todo FIXME: doesn't handle params like 'text/plain; charset=UTF-8' + * @todo FIXME: Doesn't handle params like 'text/plain; charset=UTF-8' * XXX: generalize to negotiate other stuff */ function wfNegotiateType( $cprefs, $sprefs ) { @@ -1884,28 +2194,10 @@ function wfNegotiateType( $cprefs, $sprefs ) { return $besttype; } -/** - * Array lookup - * Returns an array where the values in the first array are replaced by the - * values in the second array with the corresponding keys - * - * @return array - */ -function wfArrayLookup( $a, $b ) { - return array_flip( array_intersect( array_flip( $a ), array_keys( $b ) ) ); -} - -/** - * Convenience function; returns MediaWiki timestamp for the present time. - * @return string - */ -function wfTimestampNow() { - # return NOW - return wfTimestamp( TS_MW, time() ); -} - /** * Reference-counted warning suppression + * + * @param $end Bool */ function wfSuppressWarnings( $end = false ) { static $suppressCount = 0; @@ -1920,7 +2212,11 @@ function wfSuppressWarnings( $end = false ) { } } else { if ( !$suppressCount ) { - $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE ) ); + // E_DEPRECATED is undefined in PHP 5.2 + if( !defined( 'E_DEPRECATED' ) ){ + define( 'E_DEPRECATED', 8192 ); + } + $originalLevel = error_reporting( E_ALL & ~( E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED ) ); } ++$suppressCount; } @@ -1987,13 +2283,13 @@ define( 'TS_POSTGRES', 7 ); define( 'TS_DB2', 8 ); /** - * ISO 8601 basic format with no timezone: 19860209T200000Z - * - * This is used by ResourceLoader + * ISO 8601 basic format with no timezone: 19860209T200000Z. This is used by ResourceLoader */ define( 'TS_ISO_8601_BASIC', 9 ); /** + * Get a timestamp string in one of various formats + * * @param $outputtype Mixed: A timestamp in one of the supported formats, the * function will autodetect which format is supplied and act * accordingly. @@ -2036,7 +2332,7 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { '\d\d?[ \t\r\n]*[A-Z][a-z]{2}[ \t\r\n]*\d{2}(?:\d{2})?' . # dd Mon yyyy '[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d[ \t\r\n]*:[ \t\r\n]*\d\d/S', $ts ) ) { # hh:mm:ss # TS_RFC2822, accepting a trailing comment. See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html / r77171 - # The regex is a superset of rfc2822 for readability + # The regex is a superset of rfc2822 for readability $strtime = strtok( $ts, ';' ); } elseif ( preg_match( '/^[A-Z][a-z]{5,8}, \d\d-[A-Z][a-z]{2}-\d{2} \d\d:\d\d:\d\d/', $ts ) ) { # TS_RFC850 @@ -2046,13 +2342,11 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { $strtime = $ts; } else { # Bogus value... - wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); - + wfDebug("wfTimestamp() fed bogus time value: TYPE=$outputtype; VALUE=$ts\n"); + return false; } - - static $formats = array( TS_UNIX => 'U', TS_MW => 'YmdHis', @@ -2082,12 +2376,12 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { } else { return false; } - + if ( !$d ) { wfDebug("wfTimestamp() fed bogus time value: $outputtype; $ts\n"); return false; } - + $output = $d->format( $formats[$outputtype] ); } else { if ( count( $da ) ) { @@ -2120,6 +2414,7 @@ function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) { /** * Return a formatted timestamp, or null if input is null. * For dealing with nullable timestamp columns in the database. + * * @param $outputtype Integer * @param $ts String * @return String @@ -2132,6 +2427,16 @@ function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) { } } +/** + * Convenience function; returns MediaWiki timestamp for the present time. + * + * @return string + */ +function wfTimestampNow() { + # return NOW + return wfTimestamp( TS_MW, time() ); +} + /** * Check if the operating system is Windows * @@ -2145,8 +2450,20 @@ function wfIsWindows() { return $isWindows; } +/** + * Check if we are running under HipHop + * + * @return Bool + */ +function wfIsHipHop() { + return function_exists( 'hphp_thread_set_warmup_enabled' ); +} + /** * Swap two variables + * + * @param $x Mixed + * @param $y Mixed */ function swap( &$x, &$y ) { $z = $x; @@ -2154,116 +2471,6 @@ function swap( &$x, &$y ) { $y = $z; } -function wfGetCachedNotice( $name ) { - global $wgOut, $wgRenderHashAppend, $parserMemc; - $fname = 'wfGetCachedNotice'; - wfProfileIn( $fname ); - - $needParse = false; - - if( $name === 'default' ) { - // special case - global $wgSiteNotice; - $notice = $wgSiteNotice; - if( empty( $notice ) ) { - wfProfileOut( $fname ); - return false; - } - } else { - $notice = wfMsgForContentNoTrans( $name ); - if( wfEmptyMsg( $name, $notice ) || $notice == '-' ) { - wfProfileOut( $fname ); - return( false ); - } - } - - // Use the extra hash appender to let eg SSL variants separately cache. - $key = wfMemcKey( $name . $wgRenderHashAppend ); - $cachedNotice = $parserMemc->get( $key ); - if( is_array( $cachedNotice ) ) { - if( md5( $notice ) == $cachedNotice['hash'] ) { - $notice = $cachedNotice['html']; - } else { - $needParse = true; - } - } else { - $needParse = true; - } - - if( $needParse ) { - if( is_object( $wgOut ) ) { - $parsed = $wgOut->parse( $notice ); - $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); - $notice = $parsed; - } else { - wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' . "\n" ); - $notice = ''; - } - } - $notice = '
    ' .$notice . '
    '; - wfProfileOut( $fname ); - return $notice; -} - -function wfGetNamespaceNotice() { - global $wgTitle; - - # Paranoia - if ( !isset( $wgTitle ) || !is_object( $wgTitle ) ) { - return ''; - } - - $fname = 'wfGetNamespaceNotice'; - wfProfileIn( $fname ); - - $key = 'namespacenotice-' . $wgTitle->getNsText(); - $namespaceNotice = wfGetCachedNotice( $key ); - if ( $namespaceNotice && substr( $namespaceNotice, 0, 7 ) != '

    <' ) { - $namespaceNotice = '

    ' . $namespaceNotice . '
    '; - } else { - $namespaceNotice = ''; - } - - wfProfileOut( $fname ); - return $namespaceNotice; -} - -function wfGetSiteNotice() { - global $wgUser; - $fname = 'wfGetSiteNotice'; - wfProfileIn( $fname ); - $siteNotice = ''; - - if( wfRunHooks( 'SiteNoticeBefore', array( &$siteNotice ) ) ) { - if( is_object( $wgUser ) && $wgUser->isLoggedIn() ) { - $siteNotice = wfGetCachedNotice( 'sitenotice' ); - } else { - $anonNotice = wfGetCachedNotice( 'anonnotice' ); - if( !$anonNotice ) { - $siteNotice = wfGetCachedNotice( 'sitenotice' ); - } else { - $siteNotice = $anonNotice; - } - } - if( !$siteNotice ) { - $siteNotice = wfGetCachedNotice( 'default' ); - } - } - - wfRunHooks( 'SiteNoticeAfter', array( &$siteNotice ) ); - wfProfileOut( $fname ); - return $siteNotice; -} - -/** - * BC wrapper for MimeMagic::singleton() - * @deprecated No longer needed as of 1.17 (r68836). - */ -function &wfGetMimeMagic() { - wfDeprecated( __FUNCTION__ ); - return MimeMagic::singleton(); -} - /** * Tries to get the system directory for temporary files. The TMPDIR, TMP, and * TEMP environment variables are then checked in sequence, and if none are set @@ -2329,16 +2536,24 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { /** * Increment a statistics counter + * + * @param $key String + * @param $count Int */ -function wfIncrStats( $key ) { +function wfIncrStats( $key, $count = 1 ) { global $wgStatsMethod; + $count = intval( $count ); + if( $wgStatsMethod == 'udp' ) { - global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname; + global $wgUDPProfilerHost, $wgUDPProfilerPort, $wgDBname, $wgAggregateStatsID; static $socket; + + $id = $wgAggregateStatsID !== false ? $wgAggregateStatsID : $wgDBname; + if ( !$socket ) { $socket = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ); - $statline = "stats/{$wgDBname} - 1 1 1 1 1 -total\n"; + $statline = "stats/{$id} - {$count} 1 1 1 1 -total\n"; socket_sendto( $socket, $statline, @@ -2348,7 +2563,7 @@ function wfIncrStats( $key ) { $wgUDPProfilerPort ); } - $statline = "stats/{$wgDBname} - 1 1 1 1 1 {$key}\n"; + $statline = "stats/{$id} - {$count} 1 1 1 1 {$key}\n"; wfSuppressWarnings(); socket_sendto( $socket, @@ -2362,8 +2577,8 @@ function wfIncrStats( $key ) { } elseif( $wgStatsMethod == 'cache' ) { global $wgMemc; $key = wfMemcKey( 'stats', $key ); - if ( is_null( $wgMemc->incr( $key ) ) ) { - $wgMemc->add( $key, 1 ); + if ( is_null( $wgMemc->incr( $key, $count ) ) ) { + $wgMemc->add( $key, $count ); } } else { // Disabled @@ -2381,88 +2596,36 @@ function wfPercent( $nr, $acc = 2, $round = true ) { return $round ? round( $ret, $acc ) . '%' : "$ret%"; } -/** - * Encrypt a username/password. - * - * @param $userid Integer: ID of the user - * @param $password String: password of the user - * @return String: hashed password - * @deprecated Use User::crypt() or User::oldCrypt() instead - */ -function wfEncryptPassword( $userid, $password ) { - wfDeprecated(__FUNCTION__); - # Just wrap around User::oldCrypt() - return User::oldCrypt( $password, $userid ); -} - -/** - * Appends to second array if $value differs from that in $default - */ -function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) { - if ( is_null( $changed ) ) { - throw new MWException( 'GlobalFunctions::wfAppendToArrayIfNotDefault got null' ); - } - if ( $default[$key] !== $value ) { - $changed[$key] = $value; - } -} - -/** - * Since wfMsg() and co suck, they don't return false if the message key they - * looked up didn't exist but a XHTML string, this function checks for the - * nonexistance of messages by looking at wfMsg() output - * - * @param $key String: the message key looked up - * @return Boolean True if the message *doesn't* exist. - */ -function wfEmptyMsg( $key ) { - global $wgMessageCache; - return $wgMessageCache->get( $key, /*useDB*/true, /*content*/false ) === false; -} - /** * Find out whether or not a mixed variable exists in a string * * @param $needle String * @param $str String + * @param $insensitive Boolean * @return Boolean */ -function in_string( $needle, $str ) { - return strpos( $str, $needle ) !== false; -} +function in_string( $needle, $str, $insensitive = false ) { + $func = 'strpos'; + if( $insensitive ) $func = 'stripos'; -function wfSpecialList( $page, $details ) { - global $wgContLang; - $details = $details ? ' ' . $wgContLang->getDirMark() . "($details)" : ''; - return $page . $details; + return $func( $str, $needle ) !== false; } /** - * Returns a regular expression of url protocols + * Make a list item, used by various special pages * + * @param $page String Page link + * @param $details String Text between brackets + * @param $oppositedm Boolean Add the direction mark opposite to your + * language, to display text properly * @return String */ -function wfUrlProtocols() { - global $wgUrlProtocols; - - static $retval = null; - if ( !is_null( $retval ) ) { - return $retval; - } - - // Support old-style $wgUrlProtocols strings, for backwards compatibility - // with LocalSettings files from 1.5 - if ( is_array( $wgUrlProtocols ) ) { - $protocols = array(); - foreach ( $wgUrlProtocols as $protocol ) { - $protocols[] = preg_quote( $protocol, '/' ); - } - - $retval = implode( '|', $protocols ); - } else { - $retval = $wgUrlProtocols; - } - return $retval; +function wfSpecialList( $page, $details, $oppositedm = true ) { + global $wgLang; + $dirmark = ( $oppositedm ? $wgLang->getDirMark( true ) : '' ) . + $wgLang->getDirMark(); + $details = $details ? $dirmark . " ($details)" : ''; + return $page . $details; } /** @@ -2503,19 +2666,30 @@ function wfIniGetBool( $setting ) { * * @param $extension String A PHP extension. The file suffix (.so or .dll) * should be omitted + * @param $fileName String Name of the library, if not $extension.suffix * @return Bool - Whether or not the extension is loaded */ -function wfDl( $extension ) { +function wfDl( $extension, $fileName = null ) { if( extension_loaded( $extension ) ) { return true; } - $canDl = ( function_exists( 'dl' ) && is_callable( 'dl' ) + $canDl = false; + $sapi = php_sapi_name(); + if( version_compare( PHP_VERSION, '5.3.0', '<' ) || + $sapi == 'cli' || $sapi == 'cgi' || $sapi == 'embed' ) + { + $canDl = ( function_exists( 'dl' ) && is_callable( 'dl' ) && wfIniGetBool( 'enable_dl' ) && !wfIniGetBool( 'safe_mode' ) ); + } if( $canDl ) { + $fileName = $fileName ? $fileName : $extension; + if( wfIsWindows() ) { + $fileName = 'php_' . $fileName; + } wfSuppressWarnings(); - dl( $extension . '.' . PHP_SHLIB_SUFFIX ); + dl( $fileName . '.' . PHP_SHLIB_SUFFIX ); wfRestoreWarnings(); } return extension_loaded( $extension ); @@ -2678,208 +2852,78 @@ function wfUseMW( $req_ver ) { * Return the final portion of a pathname. * Reimplemented because PHP5's basename() is buggy with multibyte text. * http://bugs.php.net/bug.php?id=33898 - * - * PHP's basename() only considers '\' a pathchar on Windows and Netware. - * We'll consider it so always, as we don't want \s in our Unix paths either. - * - * @param $path String - * @param $suffix String: to remove if present - * @return String - */ -function wfBaseName( $path, $suffix = '' ) { - $encSuffix = ( $suffix == '' ) - ? '' - : ( '(?:' . preg_quote( $suffix, '#' ) . ')?' ); - $matches = array(); - if( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { - return $matches[1]; - } else { - return ''; - } -} - -/** - * Generate a relative path name to the given file. - * May explode on non-matching case-insensitive paths, - * funky symlinks, etc. - * - * @param $path String: absolute destination path including target filename - * @param $from String: Absolute source path, directory only - * @return String - */ -function wfRelativePath( $path, $from ) { - // Normalize mixed input on Windows... - $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); - $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); - - // Trim trailing slashes -- fix for drive root - $path = rtrim( $path, DIRECTORY_SEPARATOR ); - $from = rtrim( $from, DIRECTORY_SEPARATOR ); - - $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); - $against = explode( DIRECTORY_SEPARATOR, $from ); - - if( $pieces[0] !== $against[0] ) { - // Non-matching Windows drive letters? - // Return a full path. - return $path; - } - - // Trim off common prefix - while( count( $pieces ) && count( $against ) - && $pieces[0] == $against[0] ) { - array_shift( $pieces ); - array_shift( $against ); - } - - // relative dots to bump us to the parent - while( count( $against ) ) { - array_unshift( $pieces, '..' ); - array_shift( $against ); - } - - array_push( $pieces, wfBaseName( $path ) ); - - return implode( DIRECTORY_SEPARATOR, $pieces ); -} - -/** - * Backwards array plus for people who haven't bothered to read the PHP manual - * XXX: will not darn your socks for you. - * - * @param $array1 Array - * @param [$array2, [...]] Arrays - * @return Array - */ -function wfArrayMerge( $array1/* ... */ ) { - $args = func_get_args(); - $args = array_reverse( $args, true ); - $out = array(); - foreach ( $args as $arg ) { - $out += $arg; - } - return $out; -} - -/** - * Merge arrays in the style of getUserPermissionsErrors, with duplicate removal - * e.g. - * wfMergeErrorArrays( - * array( array( 'x' ) ), - * array( array( 'x', '2' ) ), - * array( array( 'x' ) ), - * array( array( 'y') ) - * ); - * returns: - * array( - * array( 'x', '2' ), - * array( 'x' ), - * array( 'y' ) - * ) - */ -function wfMergeErrorArrays( /*...*/ ) { - $args = func_get_args(); - $out = array(); - foreach ( $args as $errors ) { - foreach ( $errors as $params ) { - # FIXME: sometimes get nested arrays for $params, - # which leads to E_NOTICEs - $spec = implode( "\t", $params ); - $out[$spec] = $params; - } - } - return array_values( $out ); -} - -/** - * parse_url() work-alike, but non-broken. Differences: - * - * 1) Does not raise warnings on bad URLs (just returns false) - * 2) Handles protocols that don't use :// (e.g., mailto: and news:) correctly - * 3) Adds a "delimiter" element to the array, either '://' or ':' (see (2)) - * - * @param $url String: a URL to parse - * @return Array: bits of the URL in an associative array, per PHP docs - */ -function wfParseUrl( $url ) { - global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php - wfSuppressWarnings(); - $bits = parse_url( $url ); - wfRestoreWarnings(); - if ( !$bits ) { - return false; - } - - // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it - if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) { - $bits['delimiter'] = '://'; - } elseif ( in_array( $bits['scheme'] . ':', $wgUrlProtocols ) ) { - $bits['delimiter'] = ':'; - // parse_url detects for news: and mailto: the host part of an url as path - // We have to correct this wrong detection - if ( isset( $bits['path'] ) ) { - $bits['host'] = $bits['path']; - $bits['path'] = ''; - } + * + * PHP's basename() only considers '\' a pathchar on Windows and Netware. + * We'll consider it so always, as we don't want \s in our Unix paths either. + * + * @param $path String + * @param $suffix String: to remove if present + * @return String + */ +function wfBaseName( $path, $suffix = '' ) { + $encSuffix = ( $suffix == '' ) + ? '' + : ( '(?:' . preg_quote( $suffix, '#' ) . ')?' ); + $matches = array(); + if( preg_match( "#([^/\\\\]*?){$encSuffix}[/\\\\]*$#", $path, $matches ) ) { + return $matches[1]; } else { - return false; + return ''; } - - return $bits; } /** - * Make a URL index, appropriate for the el_index field of externallinks. + * Generate a relative path name to the given file. + * May explode on non-matching case-insensitive paths, + * funky symlinks, etc. + * + * @param $path String: absolute destination path including target filename + * @param $from String: Absolute source path, directory only + * @return String */ -function wfMakeUrlIndex( $url ) { - $bits = wfParseUrl( $url ); +function wfRelativePath( $path, $from ) { + // Normalize mixed input on Windows... + $path = str_replace( '/', DIRECTORY_SEPARATOR, $path ); + $from = str_replace( '/', DIRECTORY_SEPARATOR, $from ); - // Reverse the labels in the hostname, convert to lower case - // For emails reverse domainpart only - if ( $bits['scheme'] == 'mailto' ) { - $mailparts = explode( '@', $bits['host'], 2 ); - if ( count( $mailparts ) === 2 ) { - $domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) ); - } else { - // No domain specified, don't mangle it - $domainpart = ''; - } - $reversedHost = $domainpart . '@' . $mailparts[0]; - } else { - $reversedHost = strtolower( implode( '.', array_reverse( explode( '.', $bits['host'] ) ) ) ); - } - // Add an extra dot to the end - // Why? Is it in wrong place in mailto links? - if ( substr( $reversedHost, -1, 1 ) !== '.' ) { - $reversedHost .= '.'; - } - // Reconstruct the pseudo-URL - $prot = $bits['scheme']; - $index = $prot . $bits['delimiter'] . $reversedHost; - // Leave out user and password. Add the port, path, query and fragment - if ( isset( $bits['port'] ) ) { - $index .= ':' . $bits['port']; - } - if ( isset( $bits['path'] ) ) { - $index .= $bits['path']; - } else { - $index .= '/'; + // Trim trailing slashes -- fix for drive root + $path = rtrim( $path, DIRECTORY_SEPARATOR ); + $from = rtrim( $from, DIRECTORY_SEPARATOR ); + + $pieces = explode( DIRECTORY_SEPARATOR, dirname( $path ) ); + $against = explode( DIRECTORY_SEPARATOR, $from ); + + if( $pieces[0] !== $against[0] ) { + // Non-matching Windows drive letters? + // Return a full path. + return $path; } - if ( isset( $bits['query'] ) ) { - $index .= '?' . $bits['query']; + + // Trim off common prefix + while( count( $pieces ) && count( $against ) + && $pieces[0] == $against[0] ) { + array_shift( $pieces ); + array_shift( $against ); } - if ( isset( $bits['fragment'] ) ) { - $index .= '#' . $bits['fragment']; + + // relative dots to bump us to the parent + while( count( $against ) ) { + array_unshift( $pieces, '..' ); + array_shift( $against ); } - return $index; + + array_push( $pieces, wfBaseName( $path ) ); + + return implode( DIRECTORY_SEPARATOR, $pieces ); } /** * Do any deferred updates and clear the list * - * @param $commit Boolean: commit after every update to prevent lock contention + * @param $commit String: set to 'commit' to commit after every update to + * prevent lock contention */ -function wfDoUpdates( $commit = false ) { +function wfDoUpdates( $commit = '' ) { global $wgDeferredUpdateList; wfProfileIn( __METHOD__ ); @@ -2890,14 +2934,15 @@ function wfDoUpdates( $commit = false ) { return; } - if ( $commit ) { + $doCommit = $commit == 'commit'; + if ( $doCommit ) { $dbw = wfGetDB( DB_MASTER ); } foreach ( $wgDeferredUpdateList as $update ) { $update->doUpdate(); - if ( $commit && $dbw->trxLevel() ) { + if ( $doCommit && $dbw->trxLevel() ) { $dbw->commit(); } } @@ -2941,7 +2986,7 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t // Decode and validate input string $input = strtolower( $input ); for( $i = 0; $i < strlen( $input ); $i++ ) { - $n = strpos( $digitChars, $input{$i} ); + $n = strpos( $digitChars, $input[$i] ); if( $n === false || $n > $sourceBase ) { return false; } @@ -2995,36 +3040,18 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad = 1, $lowercase = t /** * Create an object with a given name and an array of construct parameters + * * @param $name String * @param $p Array: parameters + * @deprecated since 1.18, warnings in 1.18, removal in 1.20 */ function wfCreateObject( $name, $p ) { - $p = array_values( $p ); - switch ( count( $p ) ) { - case 0: - return new $name; - case 1: - return new $name( $p[0] ); - case 2: - return new $name( $p[0], $p[1] ); - case 3: - return new $name( $p[0], $p[1], $p[2] ); - case 4: - return new $name( $p[0], $p[1], $p[2], $p[3] ); - case 5: - return new $name( $p[0], $p[1], $p[2], $p[3], $p[4] ); - case 6: - return new $name( $p[0], $p[1], $p[2], $p[3], $p[4], $p[5] ); - default: - throw new MWException( 'Too many arguments to construtor in wfCreateObject' ); - } + wfDeprecated( __FUNCTION__ ); + return MWFunction::newObj( $name, $p ); } function wfHttpOnlySafe() { global $wgHttpOnlyBlacklist; - if( !version_compare( '5.2', PHP_VERSION, '<' ) ) { - return false; - } if( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { foreach( $wgHttpOnlyBlacklist as $regex ) { @@ -3039,18 +3066,31 @@ function wfHttpOnlySafe() { /** * Initialise php session + * + * @param $sessionId Bool */ function wfSetupSession( $sessionId = false ) { global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler; if( $wgSessionsInMemcached ) { - require_once( 'MemcachedSessions.php' ); + if ( !defined( 'MW_COMPILED' ) ) { + global $IP; + require_once( "$IP/includes/cache/MemcachedSessions.php" ); + } + session_set_save_handler( 'memsess_open', 'memsess_close', 'memsess_read', + 'memsess_write', 'memsess_destroy', 'memsess_gc' ); + + // It's necessary to register a shutdown function to call session_write_close(), + // because by the time the request shutdown function for the session module is + // called, $wgMemc has already been destroyed. Shutdown functions registered + // this way are called before object destruction. + register_shutdown_function( 'memsess_write_close' ); } elseif( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler # hasn't already been set to the desired value (that causes errors) ini_set( 'session.save_handler', $wgSessionHandler ); } - $httpOnlySafe = wfHttpOnlySafe(); + $httpOnlySafe = wfHttpOnlySafe() && $wgCookieHttpOnly; wfDebugLog( 'cookie', 'session_set_cookie_params: "' . implode( '", "', array( @@ -3058,13 +3098,8 @@ function wfSetupSession( $sessionId = false ) { $wgCookiePath, $wgCookieDomain, $wgCookieSecure, - $httpOnlySafe && $wgCookieHttpOnly ) ) . '"' ); - if( $httpOnlySafe && $wgCookieHttpOnly ) { - session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); - } else { - // PHP 5.1 throws warnings if you pass the HttpOnly parameter for 5.2. - session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure ); - } + $httpOnlySafe ) ) . '"' ); + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $httpOnlySafe ); session_cache_limiter( 'private, must-revalidate' ); if ( $sessionId ) { session_id( $sessionId ); @@ -3077,6 +3112,7 @@ function wfSetupSession( $sessionId = false ) { /** * Get an object from the precompiled serialized directory * + * @param $name String * @return Mixed: the variable on success, false on failure */ function wfGetPrecompiledData( $name ) { @@ -3092,43 +3128,11 @@ function wfGetPrecompiledData( $name ) { return false; } -function wfGetCaller( $level = 2 ) { - $backtrace = wfDebugBacktrace(); - if ( isset( $backtrace[$level] ) ) { - return wfFormatStackFrame( $backtrace[$level] ); - } else { - $caller = 'unknown'; - } - return $caller; -} - -/** - * Return a string consisting of callers in the stack. Useful sometimes - * for profiling specific points. - * - * @param $limit The maximum depth of the stack frame to return, or false for - * the entire stack. - */ -function wfGetAllCallers( $limit = 3 ) { - $trace = array_reverse( wfDebugBacktrace() ); - if ( !$limit || $limit > count( $trace ) - 1 ) { - $limit = count( $trace ) - 1; - } - $trace = array_slice( $trace, -$limit - 1, $limit ); - return implode( '/', array_map( 'wfFormatStackFrame', $trace ) ); -} - -/** - * Return a string representation of frame - */ -function wfFormatStackFrame( $frame ) { - return isset( $frame['class'] ) ? - $frame['class'] . '::' . $frame['function'] : - $frame['function']; -} - /** * Get a cache key + * + * @param varargs + * @return String */ function wfMemcKey( /*... */ ) { $args = func_get_args(); @@ -3139,6 +3143,11 @@ function wfMemcKey( /*... */ ) { /** * Get a cache key for a foreign DB + * + * @param $db String + * @param $prefix String + * @param varargs String + * @return String */ function wfForeignMemcKey( $db, $prefix /*, ... */ ) { $args = array_slice( func_get_args(), 2 ); @@ -3153,6 +3162,8 @@ function wfForeignMemcKey( $db, $prefix /*, ... */ ) { /** * Get an ASCII string identifying this wiki * This is used as a prefix in memcached keys + * + * @return String */ function wfWikiID() { global $wgDBprefix, $wgDBname; @@ -3165,6 +3176,9 @@ function wfWikiID() { /** * Split a wiki ID into DB name and table prefix + * + * @param $wiki String + * @param $bits String */ function wfSplitWikiID( $wiki ) { $bits = explode( '-', $wiki, 2 ); @@ -3176,6 +3190,7 @@ function wfSplitWikiID( $wiki ) { /** * Get a Database object. + * * @param $db Integer: index of the connection to get. May be DB_MASTER for the * master (for write queries), DB_SLAVE for potentially lagged read * queries, or an integer >= 0 for a particular server. @@ -3190,6 +3205,9 @@ function wfSplitWikiID( $wiki ) { * will always return the same object, unless the underlying connection or load * balancer is manually destroyed. * + * Note 2: use $this->getDB() in maintenance scripts that may be invoked by + * updater to ensure that a proper database is being updated. + * * @return DatabaseBase */ function &wfGetDB( $db, $groups = array(), $wiki = false ) { @@ -3208,6 +3226,8 @@ function wfGetLB( $wiki = false ) { /** * Get the load balancer factory object + * + * @return LBFactory */ function &wfGetLBFactory() { return LBFactory::singleton(); @@ -3216,6 +3236,7 @@ function &wfGetLBFactory() { /** * Find a file. * Shortcut for RepoGroup::singleton()->findFile() + * * @param $title String or Title object * @param $options Associative array of options: * time: requested time for an archived image, or false for the @@ -3239,6 +3260,7 @@ function wfFindFile( $title, $options = array() ) { /** * Get an object referring to a locally registered file. * Returns a valid placeholder object if the file does not exist. + * * @param $title Title or String * @return File, or null if passed an invalid Title */ @@ -3250,6 +3272,7 @@ function wfLocalFile( $title ) { * Should low-performance queries be disabled? * * @return Boolean + * @codeCoverageIgnore */ function wfQueriesMustScale() { global $wgMiserMode; @@ -3307,14 +3330,16 @@ function wfBoolToStr( $value ) { /** * Load an extension messages file - * @deprecated in 1.16 (warnings in 1.18, removed in ?) + * + * @deprecated since 1.16, warnings in 1.18, remove in 1.20 + * @codeCoverageIgnore */ -function wfLoadExtensionMessages( $extensionName, $langcode = false ) { +function wfLoadExtensionMessages() { + wfDeprecated( __FUNCTION__ ); } /** - * Get a platform-independent path to the null file, e.g. - * /dev/null + * Get a platform-independent path to the null file, e.g. /dev/null * * @return string */ @@ -3324,28 +3349,9 @@ function wfGetNull() { : '/dev/null'; } -/** - * Displays a maxlag error - * - * @param $host String: server that lags the most - * @param $lag Integer: maxlag (actual) - * @param $maxLag Integer: maxlag (requested) - */ -function wfMaxlagError( $host, $lag, $maxLag ) { - global $wgShowHostnames; - header( 'HTTP/1.1 503 Service Unavailable' ); - header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); - header( 'X-Database-Lag: ' . intval( $lag ) ); - header( 'Content-Type: text/plain' ); - if( $wgShowHostnames ) { - echo "Waiting for $host: $lag seconds lagged\n"; - } else { - echo "Waiting for a database server: $lag seconds lagged\n"; - } -} - /** * Throws a warning that $function is deprecated + * * @param $function String * @return null */ @@ -3362,32 +3368,33 @@ function wfDeprecated( $function ) { * $wgDevelopmentWarnings * * @param $msg String: message to send - * @param $callerOffset Integer: number of itmes to go back in the backtrace to + * @param $callerOffset Integer: number of items to go back in the backtrace to * find the correct caller (1 = function calling wfWarn, ...) * @param $level Integer: PHP error level; only used when $wgDevelopmentWarnings * is true */ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { + global $wgDevelopmentWarnings; + $callers = wfDebugBacktrace(); - if( isset( $callers[$callerOffset + 1] ) ){ + if ( isset( $callers[$callerOffset + 1] ) ) { $callerfunc = $callers[$callerOffset + 1]; $callerfile = $callers[$callerOffset]; - if( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { + if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) { $file = $callerfile['file'] . ' at line ' . $callerfile['line']; } else { $file = '(internal function)'; } $func = ''; - if( isset( $callerfunc['class'] ) ) { + if ( isset( $callerfunc['class'] ) ) { $func .= $callerfunc['class'] . '::'; } - if( isset( $callerfunc['function'] ) ) { + if ( isset( $callerfunc['function'] ) ) { $func .= $callerfunc['function']; } $msg .= " [Called from $func in $file]"; } - global $wgDevelopmentWarnings; if ( $wgDevelopmentWarnings ) { trigger_error( $msg, $level ); } else { @@ -3396,44 +3403,35 @@ function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) { } /** - * Sleep until the worst slave's replication lag is less than or equal to - * $maxLag, in seconds. Use this when updating very large numbers of rows, as + * Modern version of wfWaitForSlaves(). Instead of looking at replication lag + * and waiting for it to go down, this waits for the slaves to catch up to the + * master position. Use this when updating very large numbers of rows, as * in maintenance scripts, to avoid causing too much lag. Of course, this is * a no-op if there are no slaves. * - * Every time the function has to wait for a slave, it will print a message to - * that effect (and then sleep for a little while), so it's probably not best - * to use this outside maintenance scripts in its present form. - * - * @param $maxLag Integer + * @param $maxLag Integer (deprecated) * @param $wiki mixed Wiki identifier accepted by wfGetLB * @return null */ -function wfWaitForSlaves( $maxLag, $wiki = false ) { - if( $maxLag ) { - $lb = wfGetLB( $wiki ); - list( $host, $lag ) = $lb->getMaxLag( $wiki ); - while( $lag > $maxLag ) { - wfSuppressWarnings(); - $name = gethostbyaddr( $host ); - wfRestoreWarnings(); - if( $name !== false ) { - $host = $name; - } - print "Waiting for $host (lagged $lag seconds)...\n"; - sleep( $maxLag ); - list( $host, $lag ) = $lb->getMaxLag(); - } +function wfWaitForSlaves( $maxLag = false, $wiki = false ) { + $lb = wfGetLB( $wiki ); + // bug 27975 - Don't try to wait for slaves if there are none + // Prevents permission error when getting master position + if ( $lb->getServerCount() > 1 ) { + $dbw = $lb->getConnection( DB_MASTER ); + $pos = $dbw->getMasterPos(); + $lb->waitForAll( $pos ); } } /** * Used to be used for outputting text in the installer/updater - * @deprecated Warnings in 1.19, removal in 1.20 + * @deprecated since 1.18, warnings in 1.18, remove in 1.20 */ function wfOut( $s ) { + wfDeprecated( __METHOD__ ); global $wgCommandLineMode; - if ( $wgCommandLineMode && !defined( 'MEDIAWIKI_INSTALL' ) ) { + if ( $wgCommandLineMode ) { echo $s; } else { echo htmlspecialchars( $s ); @@ -3444,6 +3442,7 @@ function wfOut( $s ) { /** * Count down from $n to zero on the terminal, with a one-second pause * between showing each number. For use in command-line scripts. + * @codeCoverageIgnore */ function wfCountDown( $n ) { for ( $i = $n; $i >= 0; $i-- ) { @@ -3463,6 +3462,8 @@ function wfCountDown( $n ) { * Generate a random 32-character hexadecimal token. * @param $salt Mixed: some sort of salt, if necessary, to add to random * characters before hashing. + * @return string + * @codeCoverageIgnore */ function wfGenerateToken( $salt = '' ) { $salt = serialize( $salt ); @@ -3471,7 +3472,9 @@ function wfGenerateToken( $salt = '' ) { /** * Replace all invalid characters with - + * * @param $name Mixed: filename to process + * @return String */ function wfStripIllegalFilenameChars( $name ) { global $wgIllegalFileChars; @@ -3486,47 +3489,9 @@ function wfStripIllegalFilenameChars( $name ) { return $name; } -/** - * Insert array into another array after the specified *KEY* - * @param $array Array: The array. - * @param $insert Array: The array to insert. - * @param $after Mixed: The key to insert after - */ -function wfArrayInsertAfter( $array, $insert, $after ) { - // Find the offset of the element to insert after. - $keys = array_keys( $array ); - $offsetByKey = array_flip( $keys ); - - $offset = $offsetByKey[$after]; - - // Insert at the specified offset - $before = array_slice( $array, 0, $offset + 1, true ); - $after = array_slice( $array, $offset + 1, count( $array ) - $offset, true ); - - $output = $before + $insert + $after; - - return $output; -} - -/* Recursively converts the parameter (an object) to an array with the same data */ -function wfObjectToArray( $objOrArray, $recursive = true ) { - $array = array(); - if( is_object( $objOrArray ) ) { - $objOrArray = get_object_vars( $objOrArray ); - } - foreach ( $objOrArray as $key => $value ) { - if ( $recursive && ( is_object( $value ) || is_array( $value ) ) ) { - $value = wfObjectToArray( $value ); - } - - $array[$key] = $value; - } - - return $array; -} - /** * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit; + * * @return Integer value memory was set to. */ function wfMemoryLimit() { @@ -3553,6 +3518,7 @@ function wfMemoryLimit() { /** * Converts shorthand byte notation to integer form + * * @param $string String * @return Integer */ @@ -3582,19 +3548,25 @@ function wfShorthandToInteger( $string = '' ) { /** * Get the normalised IETF language tag + * See unit test for examples. + * * @param $code String: The language code. * @return $langCode String: The language code which complying with BCP 47 standards. */ function wfBCP47( $code ) { $codeSegment = explode( '-', $code ); + $codeBCP = array(); foreach ( $codeSegment as $segNo => $seg ) { if ( count( $codeSegment ) > 0 ) { + // when previous segment is x, it is a private segment and should be lc + if( $segNo > 0 && strtolower( $codeSegment[($segNo - 1)] ) == 'x') { + $codeBCP[$segNo] = strtolower( $seg ); // ISO 3166 country code - if ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) { + } elseif ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) ) { $codeBCP[$segNo] = strtoupper( $seg ); // ISO 15924 script code } elseif ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) ) { - $codeBCP[$segNo] = ucfirst( $seg ); + $codeBCP[$segNo] = ucfirst( strtolower( $seg ) ); // Use lowercase for other cases } else { $codeBCP[$segNo] = strtolower( $seg ); @@ -3608,13 +3580,89 @@ function wfBCP47( $code ) { return $langCode; } -function wfArrayMap( $function, $input ) { - $ret = array_map( $function, $input ); - foreach ( $ret as $key => $value ) { - $taint = istainted( $input[$key] ); - if ( $taint ) { - taint( $ret[$key], $taint ); +/** + * Get a cache object. + * + * @param $inputType integer Cache type, one the the CACHE_* constants. + * @return BagOStuff + */ +function wfGetCache( $inputType ) { + return ObjectCache::getInstance( $inputType ); +} + +/** + * Get the main cache object + * + * @return BagOStuff + */ +function wfGetMainCache() { + global $wgMainCacheType; + return ObjectCache::getInstance( $wgMainCacheType ); +} + +/** + * Get the cache object used by the message cache + * + * @return BagOStuff + */ +function wfGetMessageCacheStorage() { + global $wgMessageCacheType; + return ObjectCache::getInstance( $wgMessageCacheType ); +} + +/** + * Get the cache object used by the parser cache + * + * @return BagOStuff + */ +function wfGetParserCacheStorage() { + global $wgParserCacheType; + return ObjectCache::getInstance( $wgParserCacheType ); +} + +/** + * Call hook functions defined in $wgHooks + * + * @param $event String: event name + * @param $args Array: parameters passed to hook functions + * @return Boolean + */ +function wfRunHooks( $event, $args = array() ) { + return Hooks::run( $event, $args ); +} + +/** + * Wrapper around php's unpack. + * + * @param $format String: The format string (See php's docs) + * @param $data: A binary string of binary data + * @param $length integer or false: The minimun length of $data. This is to + * prevent reading beyond the end of $data. false to disable the check. + * + * Also be careful when using this function to read unsigned 32 bit integer + * because php might make it negative. + * + * @throws MWException if $data not long enough, or if unpack fails + * @return Associative array of the extracted data + */ +function wfUnpack( $format, $data, $length=false ) { + if ( $length !== false ) { + $realLen = strlen( $data ); + if ( $realLen < $length ) { + throw new MWException( "Tried to use wfUnpack on a " + . "string of length $realLen, but needed one " + . "of at least length $length." + ); } } - return $ret; + + wfSuppressWarnings(); + $result = unpack( $format, $data ); + wfRestoreWarnings(); + + if ( $result === false ) { + // If it cannot extract the packed data. + throw new MWException( "unpack could not unpack binary data" ); + } + return $result; } diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php deleted file mode 100644 index f0456a22..00000000 --- a/includes/HTMLCacheUpdate.php +++ /dev/null @@ -1,232 +0,0 @@ -mTitle = $titleTo; - $this->mTable = $table; - $this->mStart = $start; - $this->mEnd = $end; - $this->mRowsPerJob = $wgUpdateRowsPerJob; - $this->mRowsPerQuery = $wgUpdateRowsPerQuery; - $this->mCache = $this->mTitle->getBacklinkCache(); - } - - public function doUpdate() { - if ( $this->mStart || $this->mEnd ) { - $this->doPartialUpdate(); - return; - } - - # Get an estimate of the number of rows from the BacklinkCache - $numRows = $this->mCache->getNumLinks( $this->mTable ); - if ( $numRows > $this->mRowsPerJob * 2 ) { - # Do fast cached partition - $this->insertJobs(); - } else { - # Get the links from the DB - $titleArray = $this->mCache->getLinks( $this->mTable ); - # Check if the row count estimate was correct - if ( $titleArray->count() > $this->mRowsPerJob * 2 ) { - # Not correct, do accurate partition - wfDebug( __METHOD__.": row count estimate was incorrect, repartitioning\n" ); - $this->insertJobsFromTitles( $titleArray ); - } else { - $this->invalidateTitles( $titleArray ); - } - } - } - - /** - * Update some of the backlinks, defined by a page ID range - */ - protected function doPartialUpdate() { - $titleArray = $this->mCache->getLinks( $this->mTable, $this->mStart, $this->mEnd ); - if ( $titleArray->count() <= $this->mRowsPerJob * 2 ) { - # This partition is small enough, do the update - $this->invalidateTitles( $titleArray ); - } else { - # Partitioning was excessively inaccurate. Divide the job further. - # This can occur when a large number of links are added in a short - # period of time, say by updating a heavily-used template. - $this->insertJobsFromTitles( $titleArray ); - } - } - - /** - * Partition the current range given by $this->mStart and $this->mEnd, - * using a pre-calculated title array which gives the links in that range. - * Queue the resulting jobs. - */ - protected function insertJobsFromTitles( $titleArray ) { - # We make subpartitions in the sense that the start of the first job - # will be the start of the parent partition, and the end of the last - # job will be the end of the parent partition. - $jobs = array(); - $start = $this->mStart; # start of the current job - $numTitles = 0; - foreach ( $titleArray as $title ) { - $id = $title->getArticleID(); - # $numTitles is now the number of titles in the current job not - # including the current ID - if ( $numTitles >= $this->mRowsPerJob ) { - # Add a job up to but not including the current ID - $params = array( - 'table' => $this->mTable, - 'start' => $start, - 'end' => $id - 1 - ); - $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - $start = $id; - $numTitles = 0; - } - $numTitles++; - } - # Last job - $params = array( - 'table' => $this->mTable, - 'start' => $start, - 'end' => $this->mEnd - ); - $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - wfDebug( __METHOD__.": repartitioning into " . count( $jobs ) . " jobs\n" ); - - if ( count( $jobs ) < 2 ) { - # I don't think this is possible at present, but handling this case - # makes the code a bit more robust against future code updates and - # avoids a potential infinite loop of repartitioning - wfDebug( __METHOD__.": repartitioning failed!\n" ); - $this->invalidateTitles( $titleArray ); - return; - } - - Job::batchInsert( $jobs ); - } - - protected function insertJobs() { - $batches = $this->mCache->partition( $this->mTable, $this->mRowsPerJob ); - if ( !$batches ) { - return; - } - $jobs = array(); - foreach ( $batches as $batch ) { - $params = array( - 'table' => $this->mTable, - 'start' => $batch[0], - 'end' => $batch[1], - ); - $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - } - Job::batchInsert( $jobs ); - } - - /** - * Invalidate a range of pages, right now - * @deprecated - */ - public function invalidate( $startId = false, $endId = false ) { - $titleArray = $this->mCache->getLinks( $this->mTable, $startId, $endId ); - $this->invalidateTitles( $titleArray ); - } - - /** - * Invalidate an array (or iterator) of Title objects, right now - */ - protected function invalidateTitles( $titleArray ) { - global $wgUseFileCache, $wgUseSquid; - - $dbw = wfGetDB( DB_MASTER ); - $timestamp = $dbw->timestamp(); - - # Get all IDs in this query into an array - $ids = array(); - foreach ( $titleArray as $title ) { - $ids[] = $title->getArticleID(); - } - - if ( !$ids ) { - return; - } - - # Update page_touched - $batches = array_chunk( $ids, $this->mRowsPerQuery ); - foreach ( $batches as $batch ) { - $dbw->update( 'page', - array( 'page_touched' => $timestamp ), - array( 'page_id IN (' . $dbw->makeList( $batch ) . ')' ), - __METHOD__ - ); - } - - # Update squid - if ( $wgUseSquid ) { - $u = SquidUpdate::newFromTitles( $titleArray ); - $u->doUpdate(); - } - - # Update file cache - if ( $wgUseFileCache ) { - foreach ( $titleArray as $title ) { - HTMLFileCache::clearFileCache( $title ); - } - } - } - -} - -/** - * Job wrapper for HTMLCacheUpdate. Gets run whenever a related - * job gets called from the queue. - * - * @ingroup JobQueue - */ -class HTMLCacheUpdateJob extends Job { - var $table, $start, $end; - - /** - * Construct a job - * @param $title Title: the title linked to - * @param $params Array: job parameters (table, start and end page_ids) - * @param $id Integer: job id - */ - function __construct( $title, $params, $id = 0 ) { - parent::__construct( 'htmlCacheUpdate', $title, $params, $id ); - $this->table = $params['table']; - $this->start = $params['start']; - $this->end = $params['end']; - } - - public function run() { - $update = new HTMLCacheUpdate( $this->title, $this->table, $this->start, $this->end ); - $update->doUpdate(); - return true; - } -} diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php deleted file mode 100644 index 26cb147d..00000000 --- a/includes/HTMLFileCache.php +++ /dev/null @@ -1,233 +0,0 @@ -mTitle = $title; - $this->mType = ($type == 'raw' || $type == 'view' ) ? $type : false; - $this->fileCacheName(); // init name - } - - public function fileCacheName() { - if( !$this->mFileCache ) { - global $wgCacheDirectory, $wgFileCacheDirectory, $wgFileCacheDepth; - - if ( $wgFileCacheDirectory ) { - $dir = $wgFileCacheDirectory; - } elseif ( $wgCacheDirectory ) { - $dir = "$wgCacheDirectory/html"; - } else { - throw new MWException( 'Please set $wgCacheDirectory in LocalSettings.php if you wish to use the HTML file cache' ); - } - - # Store raw pages (like CSS hits) elsewhere - $subdir = ($this->mType === 'raw') ? 'raw/' : ''; - - $key = $this->mTitle->getPrefixedDbkey(); - if ( $wgFileCacheDepth > 0 ) { - $hash = md5( $key ); - for ( $i = 1; $i <= $wgFileCacheDepth; $i++ ) { - $subdir .= substr( $hash, 0, $i ) . '/'; - } - } - # Avoid extension confusion - $key = str_replace( '.', '%2E', urlencode( $key ) ); - $this->mFileCache = "{$dir}/{$subdir}{$key}.html"; - - if( $this->useGzip() ) { - $this->mFileCache .= '.gz'; - } - - wfDebug( __METHOD__ . ": {$this->mFileCache}\n" ); - } - return $this->mFileCache; - } - - public function isFileCached() { - if( $this->mType === false ) return false; - return file_exists( $this->fileCacheName() ); - } - - public function fileCacheTime() { - return wfTimestamp( TS_MW, filemtime( $this->fileCacheName() ) ); - } - - /** - * Check if pages can be cached for this request/user - * @return bool - */ - public static function useFileCache() { - global $wgUser, $wgUseFileCache, $wgShowIPinHeader, $wgRequest, $wgLang, $wgContLang; - if( !$wgUseFileCache ) return false; - // Get all query values - $queryVals = $wgRequest->getValues(); - foreach( $queryVals as $query => $val ) { - if( $query == 'title' || $query == 'curid' ) continue; - // Normal page view in query form can have action=view. - // Raw hits for pages also stored, like .css pages for example. - else if( $query == 'action' && ($val == 'view' || $val == 'raw') ) continue; - else if( $query == 'usemsgcache' && $val == 'yes' ) continue; - // Below are header setting params - else if( $query == 'maxage' || $query == 'smaxage' || $query == 'ctype' || $query == 'gen' ) - continue; - else - return false; - } - // Check for non-standard user language; this covers uselang, - // and extensions for auto-detecting user language. - $ulang = $wgLang->getCode(); - $clang = $wgContLang->getCode(); - // Check that there are no other sources of variation - return !$wgShowIPinHeader && !$wgUser->getId() && !$wgUser->getNewtalk() && $ulang == $clang; - } - - /* - * Check if up to date cache file exists - * @param $timestamp string - */ - public function isFileCacheGood( $timestamp = '' ) { - global $wgCacheEpoch; - - if( !$this->isFileCached() ) return false; - - $cachetime = $this->fileCacheTime(); - $good = $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime; - - wfDebug( __METHOD__ . ": cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n"); - return $good; - } - - public function useGzip() { - global $wgUseGzip; - return $wgUseGzip; - } - - /* In handy string packages */ - public function fetchRawText() { - return file_get_contents( $this->fileCacheName() ); - } - - public function fetchPageText() { - if( $this->useGzip() ) { - /* Why is there no gzfile_get_contents() or gzdecode()? */ - return implode( '', gzfile( $this->fileCacheName() ) ); - } else { - return $this->fetchRawText(); - } - } - - /* Working directory to/from output */ - public function loadFromFileCache() { - global $wgOut, $wgMimeType, $wgOutputEncoding, $wgLanguageCode; - wfDebug( __METHOD__ . "()\n"); - $filename = $this->fileCacheName(); - // Raw pages should handle cache control on their own, - // even when using file cache. This reduces hits from clients. - if( $this->mType !== 'raw' ) { - $wgOut->sendCacheControl(); - header( "Content-Type: $wgMimeType; charset={$wgOutputEncoding}" ); - header( "Content-Language: $wgLanguageCode" ); - } - - if( $this->useGzip() ) { - if( wfClientAcceptsGzip() ) { - header( 'Content-Encoding: gzip' ); - } else { - /* Send uncompressed */ - readgzfile( $filename ); - return; - } - } - readfile( $filename ); - $wgOut->disable(); // tell $wgOut that output is taken care of - } - - protected function checkCacheDirs() { - $filename = $this->fileCacheName(); - $mydir2 = substr($filename,0,strrpos($filename,'/')); # subdirectory level 2 - $mydir1 = substr($mydir2,0,strrpos($mydir2,'/')); # subdirectory level 1 - - wfMkdirParents( $mydir1 ); - wfMkdirParents( $mydir2 ); - } - - public function saveToFileCache( $text ) { - global $wgUseFileCache; - if( !$wgUseFileCache || strlen( $text ) < 512 ) { - // Disabled or empty/broken output (OOM and PHP errors) - return $text; - } - - wfDebug( __METHOD__ . "()\n", false); - - $this->checkCacheDirs(); - - $f = fopen( $this->fileCacheName(), 'w' ); - if($f) { - $now = wfTimestampNow(); - if( $this->useGzip() ) { - $rawtext = str_replace( '', - '\n", - $text ); - $text = gzencode( $rawtext ); - } else { - $text = str_replace( '', - '\n", - $text ); - } - fwrite( $f, $text ); - fclose( $f ); - if( $this->useGzip() ) { - if( wfClientAcceptsGzip() ) { - header( 'Content-Encoding: gzip' ); - return $text; - } else { - return $rawtext; - } - } else { - return $text; - } - } - return $text; - } - - public static function clearFileCache( $title ) { - global $wgUseFileCache; - - if ( !$wgUseFileCache ) { - return false; - } - - wfSuppressWarnings(); - - $fc = new self( $title, 'view' ); - unlink( $fc->fileCacheName() ); - - $fc = new self( $title, 'raw' ); - unlink( $fc->fileCacheName() ); - - wfRestoreWarnings(); - - return true; - } -} diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php index be912daf..948de61f 100644 --- a/includes/HTMLForm.php +++ b/includes/HTMLForm.php @@ -33,6 +33,10 @@ * 'help-message' -- message key for a message to use as a help text. * can be an array of msg key and then parameters to * the message. + * Overwrites 'help-messages'. + * 'help-messages' -- array of message key. As above, each item can + * be an array of msg key and then parameters. + * Overwrites 'help-message'. * 'required' -- passed through to the object, indicating that it * is a required field. * 'size' -- the length of text fields @@ -50,7 +54,6 @@ * TODO: Document 'section' / 'subsection' stuff */ class HTMLForm { - static $jsAdded = false; # A mapping of 'type' inputs onto standard HTMLFormField subclasses static $typeMappings = array( @@ -65,6 +68,7 @@ class HTMLForm { 'float' => 'HTMLFloatField', 'info' => 'HTMLInfoField', 'selectorother' => 'HTMLSelectOrOtherField', + 'selectandother' => 'HTMLSelectAndOtherField', 'submit' => 'HTMLSubmitField', 'hidden' => 'HTMLHiddenField', 'edittools' => 'HTMLEditTools', @@ -88,6 +92,8 @@ class HTMLForm { protected $mPre = ''; protected $mHeader = ''; protected $mFooter = ''; + protected $mSectionHeaders = array(); + protected $mSectionFooters = array(); protected $mPost = ''; protected $mId; @@ -95,6 +101,8 @@ class HTMLForm { protected $mSubmitName; protected $mSubmitText; protected $mSubmitTooltip; + + protected $mContext; // setTitle() * @param $messagePrefix String a prefix to go in front of default messages */ - public function __construct( $descriptor, $messagePrefix = '' ) { - $this->mMessagePrefix = $messagePrefix; + public function __construct( $descriptor, /*IContextSource*/ $context = null, $messagePrefix = '' ) { + if( $context instanceof IContextSource ){ + $this->mContext = $context; + $this->mTitle = false; // We don't need them to set a title + $this->mMessagePrefix = $messagePrefix; + } else { + // B/C since 1.18 + if( is_string( $context ) && $messagePrefix === '' ){ + // it's actually $messagePrefix + $this->mMessagePrefix = $context; + } + } // Expand out into a tree. $loadedDescriptor = array(); @@ -153,14 +173,9 @@ class HTMLForm { /** * Add the HTMLForm-specific JavaScript, if it hasn't been * done already. + * @deprecated since 1.18 load modules with ResourceLoader instead */ - static function addJS() { - if ( self::$jsAdded ) return; - - global $wgOut; - - $wgOut->addModules( 'mediawiki.legacy.htmlform' ); - } + static function addJS() { } /** * Initialise a new Object for the field @@ -173,12 +188,14 @@ class HTMLForm { } elseif ( isset( $descriptor['type'] ) ) { $class = self::$typeMappings[$descriptor['type']]; $descriptor['class'] = $class; + } else { + $class = null; } if ( !$class ) { throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) ); } - + $descriptor['fieldname'] = $fieldname; $obj = new $class( $descriptor ); @@ -191,27 +208,23 @@ class HTMLForm { */ function prepareForm() { # Check if we have the info we need - if ( ! $this->mTitle ) { + if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) { throw new MWException( "You must call setTitle() on an HTMLForm" ); } - // FIXME shouldn't this be closer to displayForm() ? - self::addJS(); - # Load data from the request. $this->loadData(); } /** * Try submitting, with edit token check first - * @return Status|boolean + * @return Status|boolean */ function tryAuthorizedSubmit() { - global $wgUser, $wgRequest; - $editToken = $wgRequest->getVal( 'wpEditToken' ); + $editToken = $this->getRequest()->getVal( 'wpEditToken' ); $result = false; - if ( $this->getMethod() != 'post' || $wgUser->matchEditToken( $editToken ) ) { + if ( $this->getMethod() != 'post' || $this->getUser()->matchEditToken( $editToken ) ) { $result = $this->trySubmit(); } return $result; @@ -304,14 +317,34 @@ class HTMLForm { /** * Add header text, inside the form. * @param $msg String complete text of message to display + * @param $section The section to add the header to */ - function addHeaderText( $msg ) { $this->mHeader .= $msg; } + function addHeaderText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mHeader .= $msg; + } else { + if ( !isset( $this->mSectionHeaders[$section] ) ) { + $this->mSectionHeaders[$section] = ''; + } + $this->mSectionHeaders[$section] .= $msg; + } + } /** * Add footer text, inside the form. * @param $msg String complete text of message to display + * @param $section string The section to add the footer text to */ - function addFooterText( $msg ) { $this->mFooter .= $msg; } + function addFooterText( $msg, $section = null ) { + if ( is_null( $section ) ) { + $this->mFooter .= $msg; + } else { + if ( !isset( $this->mSectionFooters[$section] ) ) { + $this->mSectionFooters[$section] = ''; + } + $this->mSectionFooters[$section] .= $msg; + } + } /** * Add text to the end of the display. @@ -340,10 +373,9 @@ class HTMLForm { * @param $submitResult Mixed output from HTMLForm::trySubmit() */ function displayForm( $submitResult ) { - global $wgOut; - # For good measure (it is the default) - $wgOut->preventClickjacking(); + $this->getOutput()->preventClickjacking(); + $this->getOutput()->addModules( 'mediawiki.htmlform' ); $html = '' . $this->getErrors( $submitResult ) @@ -356,7 +388,7 @@ class HTMLForm { $html = $this->wrapForm( $html ); - $wgOut->addHTML( '' + $this->getOutput()->addHTML( '' . $this->mPre . $html . $this->mPost @@ -397,12 +429,15 @@ class HTMLForm { * @return String HTML. */ function getHiddenFields() { - global $wgUser; + global $wgUsePathInfo; $html = ''; - if( $this->getMethod() == 'post' ){ - $html .= Html::hidden( 'wpEditToken', $wgUser->editToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; + $html .= Html::hidden( 'wpEditToken', $this->getUser()->editToken(), array( 'id' => 'wpEditToken' ) ) . "\n"; + $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; + } + + if ( !$wgUsePathInfo && $this->getMethod() == 'get' ) { $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n"; } @@ -431,8 +466,7 @@ class HTMLForm { } if ( isset( $this->mSubmitTooltip ) ) { - global $wgUser; - $attribs += $wgUser->getSkin()->tooltipAndAccessKeyAttribs( $this->mSubmitTooltip ); + $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip ); } $attribs['class'] = 'mw-htmlform-submit'; @@ -479,16 +513,15 @@ class HTMLForm { /** * Format and display an error message stack. - * @param $errors Mixed String or Array of message keys + * @param $errors String|Array|Status * @return String */ function getErrors( $errors ) { if ( $errors instanceof Status ) { - global $wgOut; if ( $errors->isOK() ) { $errorstr = ''; } else { - $errorstr = $wgOut->parse( $errors->getWikiText() ); + $errorstr = $this->getOutput()->parse( $errors->getWikiText() ); } } elseif ( is_array( $errors ) ) { $errorstr = $this->formatErrors( $errors ); @@ -506,7 +539,7 @@ class HTMLForm { * @param $errors Array of message keys/values * @return String HTML, a